Skip to main content

busbar_sf_agentscript/graph/render/
graphml.rs

1//! GraphML export for graph visualization.
2//!
3//! GraphML is an XML-based format for graph exchange that is widely supported
4//! by graph visualization tools like yEd, Gephi, Cytoscape, etc.
5
6use super::super::{RefGraph, RefNode};
7use petgraph::visit::EdgeRef;
8use std::fmt::Write;
9
10type NodeAttrs<'a> = (
11    &'static str,
12    Option<&'a str>,
13    Option<&'a str>,
14    Option<&'a str>,
15    Option<bool>,
16    (usize, usize),
17);
18
19/// Render a RefGraph as GraphML XML.
20///
21/// The output includes:
22/// - Node attributes: node_type, name, topic, target, mutable, span
23/// - Edge attributes: edge_type
24/// - yEd-compatible metadata keys
25pub fn render_graphml(graph: &RefGraph) -> String {
26    let inner = graph.inner();
27    let mut output = String::new();
28
29    // XML header and GraphML schema
30    writeln!(output, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
31    writeln!(output, r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#).unwrap();
32    writeln!(output, r#"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#).unwrap();
33    writeln!(output, r#"         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns"#)
34        .unwrap();
35    writeln!(output, r#"         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#)
36        .unwrap();
37
38    // Define node attributes
39    writeln!(
40        output,
41        r#"  <key id="node_type" for="node" attr.name="node_type" attr.type="string"/>"#
42    )
43    .unwrap();
44    writeln!(output, r#"  <key id="name" for="node" attr.name="name" attr.type="string"/>"#)
45        .unwrap();
46    writeln!(output, r#"  <key id="topic" for="node" attr.name="topic" attr.type="string"/>"#)
47        .unwrap();
48    writeln!(
49        output,
50        r#"  <key id="target" for="node" attr.name="target" attr.type="string"/>"#
51    )
52    .unwrap();
53    writeln!(
54        output,
55        r#"  <key id="mutable" for="node" attr.name="mutable" attr.type="boolean"/>"#
56    )
57    .unwrap();
58    writeln!(
59        output,
60        r#"  <key id="span_start" for="node" attr.name="span_start" attr.type="int"/>"#
61    )
62    .unwrap();
63    writeln!(
64        output,
65        r#"  <key id="span_end" for="node" attr.name="span_end" attr.type="int"/>"#
66    )
67    .unwrap();
68    writeln!(output, r#"  <key id="label" for="node" attr.name="label" attr.type="string"/>"#)
69        .unwrap();
70
71    // Define edge attributes
72    writeln!(
73        output,
74        r#"  <key id="edge_type" for="edge" attr.name="edge_type" attr.type="string"/>"#
75    )
76    .unwrap();
77
78    // yEd-specific: node graphics (optional, for better visualization)
79    writeln!(output, r#"  <key id="nodegraphics" for="node" yfiles.type="nodegraphics"/>"#)
80        .unwrap();
81    writeln!(output, r#"  <key id="edgegraphics" for="edge" yfiles.type="edgegraphics"/>"#)
82        .unwrap();
83
84    // Start graph
85    writeln!(output, r#"  <graph id="G" edgedefault="directed">"#).unwrap();
86
87    // Output nodes
88    for idx in inner.node_indices() {
89        if let Some(node) = graph.get_node(idx) {
90            let id = idx.index();
91            let (node_type, name, topic, target, mutable, span) = extract_node_attrs(node);
92
93            let label = node.label();
94            let escaped_label = escape_xml(&label);
95
96            writeln!(output, r#"    <node id="n{}">"#, id).unwrap();
97            writeln!(output, r#"      <data key="node_type">{}</data>"#, node_type).unwrap();
98            writeln!(output, r#"      <data key="label">{}</data>"#, escaped_label).unwrap();
99
100            if let Some(n) = name {
101                writeln!(output, r#"      <data key="name">{}</data>"#, escape_xml(n)).unwrap();
102            }
103            if let Some(t) = topic {
104                writeln!(output, r#"      <data key="topic">{}</data>"#, escape_xml(t)).unwrap();
105            }
106            if let Some(tgt) = target {
107                writeln!(output, r#"      <data key="target">{}</data>"#, escape_xml(tgt)).unwrap();
108            }
109            if let Some(m) = mutable {
110                writeln!(output, r#"      <data key="mutable">{}</data>"#, m).unwrap();
111            }
112            writeln!(output, r#"      <data key="span_start">{}</data>"#, span.0).unwrap();
113            writeln!(output, r#"      <data key="span_end">{}</data>"#, span.1).unwrap();
114
115            writeln!(output, r#"    </node>"#).unwrap();
116        }
117    }
118
119    // Output edges
120    for (edge_id, edge) in inner.edge_references().enumerate() {
121        let source = edge.source().index();
122        let target = edge.target().index();
123        let edge_type = edge.weight().label();
124
125        writeln!(
126            output,
127            r#"    <edge id="e{}" source="n{}" target="n{}">"#,
128            edge_id, source, target
129        )
130        .unwrap();
131        writeln!(output, r#"      <data key="edge_type">{}</data>"#, edge_type).unwrap();
132        writeln!(output, r#"    </edge>"#).unwrap();
133    }
134
135    // Close graph and graphml
136    writeln!(output, r#"  </graph>"#).unwrap();
137    writeln!(output, r#"</graphml>"#).unwrap();
138
139    output
140}
141
142/// Extract node attributes for GraphML output.
143fn extract_node_attrs(node: &RefNode) -> NodeAttrs<'_> {
144    match node {
145        RefNode::StartAgent { span } => ("start_agent", None, None, None, None, *span),
146        RefNode::Topic { name, span } => ("topic", Some(name.as_str()), None, None, None, *span),
147        RefNode::ActionDef { name, topic, span } => {
148            ("action_def", Some(name.as_str()), Some(topic.as_str()), None, None, *span)
149        }
150        RefNode::ReasoningAction {
151            name,
152            topic,
153            target,
154            span,
155        } => (
156            "reasoning_action",
157            Some(name.as_str()),
158            Some(topic.as_str()),
159            target.as_deref(),
160            None,
161            *span,
162        ),
163        RefNode::Variable {
164            name,
165            mutable,
166            span,
167        } => ("variable", Some(name.as_str()), None, None, Some(*mutable), *span),
168        RefNode::Connection { name, span } => {
169            ("connection", Some(name.as_str()), None, None, None, *span)
170        }
171    }
172}
173
174/// Escape special XML characters.
175fn escape_xml(s: &str) -> String {
176    s.replace('&', "&amp;")
177        .replace('<', "&lt;")
178        .replace('>', "&gt;")
179        .replace('"', "&quot;")
180        .replace('\'', "&apos;")
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_escape_xml() {
189        assert_eq!(escape_xml("hello"), "hello");
190        assert_eq!(escape_xml("<tag>"), "&lt;tag&gt;");
191        assert_eq!(escape_xml("a & b"), "a &amp; b");
192        assert_eq!(escape_xml(r#"say "hello""#), "say &quot;hello&quot;");
193    }
194}