Skip to main content

mecha10_dev/services/topology/
source_scan.rs

1//! Fallback source-file parsing for topic definitions and pub/sub call sites
2
3use anyhow::{Context, Result};
4use regex::Regex;
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7
8use super::{TopicRef, TopologyService};
9
10impl TopologyService {
11    /// Parse node source file for topic definitions and usage
12    pub(super) async fn parse_node_source(&self, source_path: &Path) -> Result<(Vec<TopicRef>, Vec<TopicRef>)> {
13        let content = tokio::fs::read_to_string(source_path)
14            .await
15            .context(format!("Failed to read source file: {}", source_path.display()))?;
16
17        // Extract topic constants and their types
18        let topic_defs = self.extract_topic_definitions(&content);
19
20        // Find publish calls
21        let publishes = self.extract_publish_calls(&content, &topic_defs);
22
23        // Find subscribe calls
24        let subscribes = self.extract_subscribe_calls(&content, &topic_defs);
25
26        Ok((publishes, subscribes))
27    }
28
29    /// Extract topic constant definitions from source
30    ///
31    /// Note: This method is public primarily for testing purposes.
32    pub fn extract_topic_definitions(&self, content: &str) -> HashMap<String, TopicRef> {
33        let mut topics = HashMap::new();
34
35        // Match: pub const TOPIC_NAME: Topic<MessageType> = Topic::new("/path");
36        let topic_pattern =
37            Regex::new(r#"pub\s+const\s+([A-Z_]+):\s*Topic<([^>]+)>\s*=\s*Topic::new\("([^"]+)"\)"#).unwrap();
38
39        for caps in topic_pattern.captures_iter(content) {
40            let const_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
41            let message_type = caps.get(2).map(|m| m.as_str().trim()).unwrap_or("");
42            let topic_path = caps.get(3).map(|m| m.as_str()).unwrap_or("");
43
44            topics.insert(
45                const_name.to_string(),
46                TopicRef {
47                    path: topic_path.to_string(),
48                    message_type: Some(message_type.to_string()),
49                },
50            );
51        }
52
53        topics
54    }
55
56    /// Extract publish_to calls
57    ///
58    /// Note: This method is public primarily for testing purposes.
59    pub fn extract_publish_calls(&self, content: &str, topic_defs: &HashMap<String, TopicRef>) -> Vec<TopicRef> {
60        // Match: ctx.publish_to(TOPIC_NAME, ...)
61        // or: publish_to(TOPIC_NAME, ...)
62        // or: ctx.publish_to(topics::TOPIC_NAME, ...)
63        // Use a more flexible pattern that handles whitespace and optional module prefix
64        let publish_pattern = Regex::new(r"publish_to\s*\(\s*(?:[a-z_]+::)?([A-Z_][A-Z0-9_]*)\s*,").unwrap();
65
66        Self::extract_topic_refs(content, topic_defs, &publish_pattern)
67    }
68
69    /// Extract subscribe calls
70    ///
71    /// Note: This method is public primarily for testing purposes.
72    pub fn extract_subscribe_calls(&self, content: &str, topic_defs: &HashMap<String, TopicRef>) -> Vec<TopicRef> {
73        // Match: ctx.subscribe::<MessageType>(TOPIC_NAME)
74        // or: subscribe(TOPIC_NAME)
75        // or: ctx.subscribe::<MessageType>(topics::TOPIC_NAME)
76        // Use a more flexible pattern that handles whitespace, generics, and optional module prefix
77        let subscribe_pattern =
78            Regex::new(r"subscribe\s*(?:::\s*<[^>]+>\s*)?\(\s*(?:[a-z_]+::)?([A-Z_][A-Z0-9_]*)\s*\)").unwrap();
79
80        Self::extract_topic_refs(content, topic_defs, &subscribe_pattern)
81    }
82
83    /// Shared implementation for [`Self::extract_publish_calls`] and
84    /// [`Self::extract_subscribe_calls`], which only differ in the regex used to find call
85    /// sites referencing a topic constant.
86    fn extract_topic_refs(content: &str, topic_defs: &HashMap<String, TopicRef>, pattern: &Regex) -> Vec<TopicRef> {
87        let mut refs = Vec::new();
88        let mut seen = HashSet::new();
89
90        for caps in pattern.captures_iter(content) {
91            if let Some(const_name) = caps.get(1) {
92                let const_name = const_name.as_str();
93                if let Some(topic) = topic_defs.get(const_name) {
94                    if seen.insert(topic.path.clone()) {
95                        refs.push(topic.clone());
96                    }
97                }
98            }
99        }
100
101        refs
102    }
103}