mecha10-dev 0.6.5

Mecha10 dev services — node discovery, topology analysis, and dev mode support
Documentation
//! Fallback source-file parsing for topic definitions and pub/sub call sites

use anyhow::{Context, Result};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::path::Path;

use super::{TopicRef, TopologyService};

/// Generates a `TopologyService::extract_*_calls` method: a thin `(&self, content, topic_defs)`
/// wrapper that builds the given regex `$pattern` and forwards to
/// [`TopologyService::extract_topic_refs`]. `extract_publish_calls`/`extract_subscribe_calls`
/// only ever differ in which pattern they search for — generating them from one macro keeps
/// that single difference visible at the call site instead of duplicating the wrapper shape.
macro_rules! extract_topic_refs_fn {
    ($(#[$meta:meta])* $vis:vis fn $name:ident() -> $pattern:expr) => {
        $(#[$meta])*
        $vis fn $name(&self, content: &str, topic_defs: &HashMap<String, TopicRef>) -> Vec<TopicRef> {
            let pattern = $pattern;
            Self::extract_topic_refs(content, topic_defs, &pattern)
        }
    };
}

impl TopologyService {
    /// Parse node source file for topic definitions and usage
    pub(super) async fn parse_node_source(&self, source_path: &Path) -> Result<(Vec<TopicRef>, Vec<TopicRef>)> {
        let content = tokio::fs::read_to_string(source_path)
            .await
            .context(format!("Failed to read source file: {}", source_path.display()))?;

        let topic_defs = self.extract_topic_definitions(&content);

        let publishes = self.extract_publish_calls(&content, &topic_defs);

        let subscribes = self.extract_subscribe_calls(&content, &topic_defs);

        Ok((publishes, subscribes))
    }

    /// Extract topic constant definitions from source
    ///
    /// Note: This method is public primarily for testing purposes.
    pub fn extract_topic_definitions(&self, content: &str) -> HashMap<String, TopicRef> {
        let mut topics = HashMap::new();

        let topic_pattern =
            Regex::new(r#"pub\s+const\s+([A-Z_]+):\s*Topic<([^>]+)>\s*=\s*Topic::new\("([^"]+)"\)"#).unwrap();

        for caps in topic_pattern.captures_iter(content) {
            let const_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
            let message_type = caps.get(2).map(|m| m.as_str().trim()).unwrap_or("");
            let topic_path = caps.get(3).map(|m| m.as_str()).unwrap_or("");

            topics.insert(
                const_name.to_string(),
                TopicRef {
                    path: topic_path.to_string(),
                    message_type: Some(message_type.to_string()),
                },
            );
        }

        topics
    }

    extract_topic_refs_fn!(
        /// Extract publish_to calls
        ///
        /// Note: This method is public primarily for testing purposes.
        pub fn extract_publish_calls() -> Regex::new(r"publish_to\s*\(\s*(?:[a-z_]+::)?([A-Z_][A-Z0-9_]*)\s*,").unwrap()
    );

    extract_topic_refs_fn!(
        /// Extract subscribe calls
        ///
        /// Note: This method is public primarily for testing purposes.
        pub fn extract_subscribe_calls() -> Regex::new(r"subscribe\s*(?:::\s*<[^>]+>\s*)?\(\s*(?:[a-z_]+::)?([A-Z_][A-Z0-9_]*)\s*\)").unwrap()
    );

    /// Shared implementation for [`Self::extract_publish_calls`] and
    /// [`Self::extract_subscribe_calls`], which only differ in the regex used to find call
    /// sites referencing a topic constant.
    fn extract_topic_refs(content: &str, topic_defs: &HashMap<String, TopicRef>, pattern: &Regex) -> Vec<TopicRef> {
        let mut refs = Vec::new();
        let mut seen = HashSet::new();

        for caps in pattern.captures_iter(content) {
            if let Some(const_name) = caps.get(1) {
                let const_name = const_name.as_str();
                if let Some(topic) = topic_defs.get(const_name) {
                    if seen.insert(topic.path.clone()) {
                        refs.push(topic.clone());
                    }
                }
            }
        }

        refs
    }
}