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