infigraph_core/
sequence.rs1use std::collections::{HashMap, HashSet, VecDeque};
2
3use anyhow::Result;
4
5use crate::graph::GraphQuery;
6
7pub fn generate_sequence_mermaid(
12 gq: &GraphQuery,
13 entry_symbol_id: &str,
14 depth: u32,
15) -> Result<String> {
16 let mut visited: HashSet<String> = HashSet::new();
18 let mut queue: VecDeque<(String, u32)> = VecDeque::new();
19 let mut edges: Vec<(String, String)> = Vec::new();
21
22 queue.push_back((entry_symbol_id.to_string(), 0));
23 visited.insert(entry_symbol_id.to_string());
24
25 while let Some((id, hop)) = queue.pop_front() {
26 if hop >= depth {
27 continue;
28 }
29 let esc = id.replace('\'', "\\'");
30 let q = format!("MATCH (a:Symbol)-[:CALLS]->(b:Symbol) WHERE a.id = '{esc}' RETURN b.id");
31 if let Ok(rows) = gq.raw_query(&q) {
32 for row in &rows {
33 if let Some(callee_id) = row.first() {
34 edges.push((id.clone(), callee_id.clone()));
35 if visited.insert(callee_id.clone()) {
36 queue.push_back((callee_id.clone(), hop + 1));
37 }
38 }
39 }
40 }
41 }
42
43 if edges.is_empty() {
44 return Ok(format!(
45 "sequenceDiagram\n note over {}: no outgoing calls found\n",
46 participant_name(entry_symbol_id)
47 ));
48 }
49
50 let mut sym_info: HashMap<String, (String, String)> = HashMap::new(); for id in &visited {
53 let esc = id.replace('\'', "\\'");
54 let q = format!("MATCH (s:Symbol) WHERE s.id = '{esc}' RETURN s.name, s.file");
55 if let Ok(rows) = gq.raw_query(&q) {
56 if let Some(row) = rows.first() {
57 if row.len() >= 2 {
58 sym_info.insert(id.clone(), (row[0].clone(), row[1].clone()));
59 }
60 }
61 }
62 }
63
64 let mut participants: Vec<String> = Vec::new();
66 let mut seen_parts: HashSet<String> = HashSet::new();
67
68 if let Some((_, file)) = sym_info.get(entry_symbol_id) {
70 let p = file_to_participant(file);
71 if seen_parts.insert(p.clone()) {
72 participants.push(p);
73 }
74 }
75 for (caller, callee) in &edges {
76 for id in [caller, callee] {
77 if let Some((_, file)) = sym_info.get(id) {
78 let p = file_to_participant(file);
79 if seen_parts.insert(p.clone()) {
80 participants.push(p);
81 }
82 }
83 }
84 }
85
86 let mut out = String::from("sequenceDiagram\n");
87 for p in &participants {
88 out.push_str(&format!(" participant {p}\n"));
89 }
90 out.push('\n');
91
92 for (caller_id, callee_id) in &edges {
93 let caller_file = sym_info
94 .get(caller_id)
95 .map(|(_, f)| f.as_str())
96 .unwrap_or(caller_id);
97 let callee_file = sym_info
98 .get(callee_id)
99 .map(|(_, f)| f.as_str())
100 .unwrap_or(callee_id);
101 let caller_part = file_to_participant(caller_file);
102 let callee_part = file_to_participant(callee_file);
103 let callee_name = sym_info
104 .get(callee_id)
105 .map(|(n, _)| n.as_str())
106 .unwrap_or(callee_id);
107 out.push_str(&format!(
108 " {caller_part}->>{callee_part}: {callee_name}()\n"
109 ));
110 }
111
112 Ok(out)
113}
114
115fn file_to_participant(file: &str) -> String {
118 let stem = std::path::Path::new(file)
119 .file_stem()
120 .and_then(|s| s.to_str())
121 .unwrap_or(file);
122 stem.replace([' ', '.', '-'], "_")
124}
125
126fn participant_name(symbol_id: &str) -> String {
127 file_to_participant(symbol_id.split("::").next().unwrap_or(symbol_id))
128}