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