use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeType {
Signal,
Receiver,
Middleware,
}
#[derive(Debug, Clone)]
pub struct SignalNode {
pub id: String,
pub node_type: NodeType,
pub description: String,
pub priority: Option<i32>,
pub is_critical: bool,
}
#[derive(Debug, Clone)]
pub struct SignalEdge {
pub from: String,
pub to: String,
pub label: Option<String>,
pub is_conditional: bool,
}
pub struct SignalGraph {
nodes: HashMap<String, SignalNode>,
edges: Vec<SignalEdge>,
}
impl SignalGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
}
}
pub fn add_signal_node(&mut self, id: &str, description: &str) {
self.nodes.insert(
id.to_string(),
SignalNode {
id: id.to_string(),
node_type: NodeType::Signal,
description: description.to_string(),
priority: None,
is_critical: false,
},
);
}
pub fn add_receiver_node(&mut self, id: &str, description: &str, priority: i32) {
self.nodes.insert(
id.to_string(),
SignalNode {
id: id.to_string(),
node_type: NodeType::Receiver,
description: description.to_string(),
priority: Some(priority),
is_critical: false,
},
);
}
pub fn add_middleware_node(&mut self, id: &str, description: &str) {
self.nodes.insert(
id.to_string(),
SignalNode {
id: id.to_string(),
node_type: NodeType::Middleware,
description: description.to_string(),
priority: None,
is_critical: false,
},
);
}
pub fn mark_as_critical(&mut self, node_id: &str) {
if let Some(node) = self.nodes.get_mut(node_id) {
node.is_critical = true;
}
}
pub fn add_edge(&mut self, from: &str, to: &str, label: Option<String>) {
self.edges.push(SignalEdge {
from: from.to_string(),
to: to.to_string(),
label,
is_conditional: false,
});
}
pub fn add_conditional_edge(&mut self, from: &str, to: &str, condition: &str) {
self.edges.push(SignalEdge {
from: from.to_string(),
to: to.to_string(),
label: Some(condition.to_string()),
is_conditional: true,
});
}
pub fn to_dot(&self) -> String {
let mut output = String::from("digraph SignalGraph {\n");
output.push_str(" rankdir=LR;\n");
output.push_str(" node [shape=box, style=rounded];\n\n");
for (id, node) in &self.nodes {
let (shape, color) = match node.node_type {
NodeType::Signal => ("ellipse", "lightblue"),
NodeType::Receiver => ("box", "lightgreen"),
NodeType::Middleware => ("diamond", "lightyellow"),
};
let border_color = if node.is_critical { "red" } else { "black" };
let priority_label = node
.priority
.map(|p| format!("\\nPriority: {}", p))
.unwrap_or_default();
let escaped_id = escape_dot_label(id);
let escaped_desc = escape_dot_label(&node.description);
output.push_str(&format!(
" \"{}\" [shape={}, fillcolor={}, style=\"filled,rounded\", color={}, label=\"{}{}\\n{}\"];\n",
escaped_id, shape, color, border_color, escaped_id, priority_label, escaped_desc
));
}
output.push('\n');
for edge in &self.edges {
let style = if edge.is_conditional {
"style=dashed"
} else {
"style=solid"
};
let label = edge
.label
.as_ref()
.map(|l| format!("label=\"{}\"", escape_dot_label(l)))
.unwrap_or_default();
output.push_str(&format!(
" \"{}\" -> \"{}\" [{}{}{}];\n",
escape_dot_label(&edge.from),
escape_dot_label(&edge.to),
style,
if label.is_empty() { "" } else { ", " },
label
));
}
output.push_str("}\n");
output
}
pub fn to_mermaid(&self) -> String {
let mut output = String::from("graph LR\n");
for (id, node) in &self.nodes {
let (shape_start, shape_end) = match node.node_type {
NodeType::Signal => ("([", "])"),
NodeType::Receiver => ("[", "]"),
NodeType::Middleware => ("{", "}"),
};
let critical_marker = if node.is_critical { "âš " } else { "" };
let priority_label = node
.priority
.map(|p| format!(" P{}", p))
.unwrap_or_default();
output.push_str(&format!(
" {}{}{}{}{}{}\n",
id, shape_start, critical_marker, node.description, priority_label, shape_end
));
}
output.push('\n');
for edge in &self.edges {
let arrow = if edge.is_conditional { "-.->" } else { "-->" };
let label = edge
.label
.as_ref()
.map(|l| format!("|{}|", l))
.unwrap_or_default();
output.push_str(&format!(" {} {}{} {}\n", edge.from, arrow, label, edge.to));
}
output
}
pub fn to_ascii(&self) -> String {
let mut output = String::from("Signal Flow Diagram\n");
output.push_str("===================\n\n");
let mut signal_to_receivers: HashMap<String, Vec<String>> = HashMap::new();
for edge in &self.edges {
if let Some(from_node) = self.nodes.get(&edge.from)
&& from_node.node_type == NodeType::Signal
{
signal_to_receivers
.entry(edge.from.clone())
.or_default()
.push(edge.to.clone());
}
}
for (signal_id, receivers) in signal_to_receivers {
if let Some(signal_node) = self.nodes.get(&signal_id) {
output.push_str(&format!("({}) {}\n", signal_id, signal_node.description));
output.push_str(" |\n");
for (i, receiver_id) in receivers.iter().enumerate() {
if let Some(receiver_node) = self.nodes.get(receiver_id) {
let is_last = i == receivers.len() - 1;
let connector = if is_last {
" └──>"
} else {
" ├──>"
};
let critical = if receiver_node.is_critical {
" âš "
} else {
""
};
let priority = receiver_node
.priority
.map(|p| format!(" [P{}]", p))
.unwrap_or_default();
output.push_str(&format!(
"{} [{}] {}{}{}\n",
connector, receiver_id, receiver_node.description, priority, critical
));
}
}
output.push('\n');
}
}
output
}
pub fn nodes(&self) -> Vec<&SignalNode> {
self.nodes.values().collect()
}
pub fn edges(&self) -> &[SignalEdge] {
&self.edges
}
pub fn find_receivers(&self, signal_id: &str) -> Vec<&SignalNode> {
let receiver_ids: HashSet<_> = self
.edges
.iter()
.filter(|e| e.from == signal_id)
.map(|e| e.to.as_str())
.collect();
receiver_ids
.into_iter()
.filter_map(|id| self.nodes.get(id))
.collect()
}
pub fn find_signals_for_receiver(&self, receiver_id: &str) -> Vec<&SignalNode> {
let signal_ids: HashSet<_> = self
.edges
.iter()
.filter(|e| e.to == receiver_id)
.map(|e| e.from.as_str())
.collect();
signal_ids
.into_iter()
.filter_map(|id| self.nodes.get(id))
.collect()
}
}
impl Default for SignalGraph {
fn default() -> Self {
Self::new()
}
}
fn escape_dot_label(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_nodes() {
let mut graph = SignalGraph::new();
graph.add_signal_node("user_created", "User created signal");
graph.add_receiver_node("send_email", "Send welcome email", 10);
assert_eq!(graph.nodes.len(), 2);
}
#[test]
fn test_add_edges() {
let mut graph = SignalGraph::new();
graph.add_signal_node("signal1", "Signal 1");
graph.add_receiver_node("receiver1", "Receiver 1", 0);
graph.add_edge("signal1", "receiver1", None);
assert_eq!(graph.edges.len(), 1);
}
#[test]
fn test_to_dot() {
let mut graph = SignalGraph::new();
graph.add_signal_node("user_created", "User created");
graph.add_receiver_node("send_email", "Send email", 10);
graph.add_edge("user_created", "send_email", Some("notify".to_string()));
let dot = graph.to_dot();
assert!(dot.contains("digraph SignalGraph"));
assert!(dot.contains("user_created"));
assert!(dot.contains("send_email"));
assert!(dot.contains("notify"));
}
#[test]
fn test_to_mermaid() {
let mut graph = SignalGraph::new();
graph.add_signal_node("signal1", "Signal");
graph.add_receiver_node("receiver1", "Receiver", 0);
graph.add_edge("signal1", "receiver1", None);
let mermaid = graph.to_mermaid();
assert!(mermaid.contains("graph LR"));
assert!(mermaid.contains("signal1"));
}
#[test]
fn test_to_ascii() {
let mut graph = SignalGraph::new();
graph.add_signal_node("signal1", "Test signal");
graph.add_receiver_node("receiver1", "Test receiver", 0);
graph.add_edge("signal1", "receiver1", None);
let ascii = graph.to_ascii();
assert!(ascii.contains("signal1"));
assert!(ascii.contains("receiver1"));
}
#[test]
fn test_mark_as_critical() {
let mut graph = SignalGraph::new();
graph.add_receiver_node("payment", "Payment processor", 10);
graph.mark_as_critical("payment");
let node = graph.nodes.get("payment").unwrap();
assert!(node.is_critical);
}
#[test]
fn test_find_receivers() {
let mut graph = SignalGraph::new();
graph.add_signal_node("signal1", "Signal");
graph.add_receiver_node("receiver1", "Receiver 1", 0);
graph.add_receiver_node("receiver2", "Receiver 2", 0);
graph.add_edge("signal1", "receiver1", None);
graph.add_edge("signal1", "receiver2", None);
let receivers = graph.find_receivers("signal1");
assert_eq!(receivers.len(), 2);
}
#[test]
fn test_conditional_edge() {
let mut graph = SignalGraph::new();
graph.add_signal_node("user_action", "User action");
graph.add_receiver_node("admin_handler", "Admin handler", 0);
graph.add_conditional_edge("user_action", "admin_handler", "if admin");
let edge = &graph.edges[0];
assert!(edge.is_conditional);
}
}