mod types;
pub use types::{ChannelInfo, ConditionalEdgeInfo, EdgeInfo, GraphTopology, NodeInfo, RouteInfo};
use crate::Result;
use crate::graph::builder::{END, GraphBuilder, START};
use crate::graph::compiled::CompiledGraph;
use crate::language::{Blueprint, Routing};
struct TopologyParts {
graph_id: String,
recursion_limit: usize,
parallel: bool,
nodes: Vec<(String, Option<String>, bool)>,
edges: Vec<(String, String)>,
conditional: Vec<(String, Vec<(String, String)>)>,
channels: Vec<(String, String)>,
}
fn build_topology(parts: TopologyParts) -> GraphTopology {
let TopologyParts {
graph_id,
recursion_limit,
parallel,
nodes,
edges,
conditional,
channels,
} = parts;
let mut entry: Option<String> = None;
let mut direct: Vec<EdgeInfo> = Vec::new();
let mut finish_nodes: Vec<String> = Vec::new();
for (from, to) in edges {
if from == START {
entry = Some(to);
} else if to == END {
finish_nodes.push(from);
} else {
direct.push(EdgeInfo { from, to });
}
}
let mut node_infos: Vec<NodeInfo> = nodes
.into_iter()
.map(|(id, kind, command_routing)| NodeInfo {
id,
kind,
command_routing,
})
.collect();
let mut conditional_edges: Vec<ConditionalEdgeInfo> = conditional
.into_iter()
.map(|(from, routes)| {
let mut routes: Vec<RouteInfo> = routes
.into_iter()
.map(|(label, target)| RouteInfo { label, target })
.collect();
routes.sort();
ConditionalEdgeInfo { from, routes }
})
.collect();
let channels: Vec<ChannelInfo> = channels
.into_iter()
.map(|(name, reducer)| ChannelInfo { name, reducer })
.collect();
node_infos.sort_by(|a, b| a.id.cmp(&b.id));
direct.sort();
conditional_edges.sort_by(|a, b| a.from.cmp(&b.from));
finish_nodes.sort();
finish_nodes.dedup();
GraphTopology {
graph_id,
entry,
recursion_limit,
parallel,
nodes: node_infos,
edges: direct,
conditional_edges,
finish_nodes,
channels,
}
}
impl<State, Update> CompiledGraph<State, Update> {
pub fn topology(&self) -> GraphTopology {
let nodes = self
.nodes
.keys()
.map(|id| (id.to_string(), None, self.command_nodes.contains(id)))
.collect();
let edges = self
.edges
.iter()
.map(|(from, to)| (from.to_string(), to.to_string()))
.collect();
let conditional = self
.branches
.iter()
.map(|(from, branch)| {
let routes = branch
.routes
.iter()
.map(|(label, target)| (label.clone(), target.to_string()))
.collect();
(from.to_string(), routes)
})
.collect();
build_topology(TopologyParts {
graph_id: self.graph_id().to_string(),
recursion_limit: self.recursion_limit,
parallel: self.parallel,
nodes,
edges,
conditional,
channels: Vec::new(),
})
}
}
impl<State, Update> GraphBuilder<State, Update> {
pub fn topology(&self) -> GraphTopology {
let nodes = self
.nodes
.keys()
.map(|id| (id.to_string(), None, self.command_nodes.contains(id)))
.collect();
let edges = self
.edges
.iter()
.map(|(from, to)| (from.to_string(), to.to_string()))
.collect();
let conditional = self
.branches
.iter()
.map(|(from, branch)| {
let routes = branch
.routes
.iter()
.map(|(label, target)| (label.clone(), target.to_string()))
.collect();
(from.to_string(), routes)
})
.collect();
build_topology(TopologyParts {
graph_id: self.graph_id.to_string(),
recursion_limit: self.recursion_limit,
parallel: self.parallel,
nodes,
edges,
conditional,
channels: Vec::new(),
})
}
}
pub fn blueprint_to_topology(blueprint: &Blueprint) -> GraphTopology {
let recursion_limit = blueprint
.defaults
.iter()
.find(|(key, _)| key == "recursion_limit")
.and_then(|(_, value)| match value {
crate::language::Literal::Num(n) if *n >= 0.0 => Some(*n as usize),
_ => None,
})
.unwrap_or(0);
let nodes = blueprint
.nodes
.iter()
.map(|n| (n.name.clone(), Some(n.kind.clone()), false))
.collect();
let mut edges: Vec<(String, String)> = blueprint
.edges
.iter()
.map(|e| (e.from.clone(), e.to.clone()))
.collect();
edges.push((START.to_string(), blueprint.start.clone()));
let mut conditional: Vec<(String, Vec<(String, String)>)> = Vec::new();
for node in &blueprint.nodes {
match &node.routing {
Routing::Next(target) => edges.push((node.name.clone(), target.clone())),
Routing::Terminal => edges.push((node.name.clone(), END.to_string())),
Routing::Conditional(routes) => {
conditional.push((node.name.clone(), routes.clone()));
}
}
}
let channels = blueprint
.channels
.iter()
.map(|c| (c.name.clone(), c.reducer.clone()))
.collect();
build_topology(TopologyParts {
graph_id: blueprint.graph_id.clone(),
recursion_limit,
parallel: false,
nodes,
edges,
conditional,
channels,
})
}
pub fn to_json(topology: &GraphTopology) -> String {
serde_json::to_string_pretty(topology).unwrap_or_else(|_| "{}".to_string())
}
pub fn from_json(json: &str) -> Result<GraphTopology> {
Ok(serde_json::from_str(json)?)
}
pub fn to_mermaid(topology: &GraphTopology) -> String {
let mut out = String::from("flowchart TD\n");
out.push_str(" START([START])\n");
out.push_str(" END([END])\n");
for node in &topology.nodes {
let id = mermaid_id(&node.id);
out.push_str(&format!(" {id}[\"{}\"]\n", escape_label(&node.id)));
}
out.push('\n');
if let Some(entry) = &topology.entry {
out.push_str(&format!(" START --> {}\n", mermaid_ref(entry)));
}
for edge in &topology.edges {
out.push_str(&format!(
" {} --> {}\n",
mermaid_ref(&edge.from),
mermaid_ref(&edge.to)
));
}
for cond in &topology.conditional_edges {
for route in &cond.routes {
out.push_str(&format!(
" {} -- {} --> {}\n",
mermaid_ref(&cond.from),
escape_label(&route.label),
mermaid_ref(&route.target)
));
}
}
for node in &topology.finish_nodes {
out.push_str(&format!(" {} --> END\n", mermaid_ref(node)));
}
out
}
pub fn blueprint_to_mermaid(blueprint: &Blueprint) -> String {
to_mermaid(&blueprint_to_topology(blueprint))
}
pub fn blueprint_to_json(blueprint: &Blueprint) -> String {
to_json(&blueprint_to_topology(blueprint))
}
fn mermaid_ref(id: &str) -> String {
if id == START || id == "START" {
"START".to_string()
} else if id == END || id == "END" {
"END".to_string()
} else {
mermaid_id(id)
}
}
fn mermaid_id(id: &str) -> String {
let mut sanitized = String::with_capacity(id.len() + 2);
sanitized.push_str("n_");
for ch in id.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
sanitized.push(ch);
} else {
sanitized.push('_');
}
}
sanitized
}
fn escape_label(label: &str) -> String {
label.replace('"', """)
}
#[cfg(test)]
mod test;