Skip to main content

graphify_export/
graphml.rs

1//! GraphML XML export.
2
3use std::fmt::Write;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use graphify_core::graph::KnowledgeGraph;
8use tracing::info;
9
10/// Export the graph to GraphML format.
11pub fn export_graphml(graph: &KnowledgeGraph, output_dir: &Path) -> anyhow::Result<PathBuf> {
12    let mut xml = String::with_capacity(4096);
13
14    writeln!(xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
15    writeln!(
16        xml,
17        r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
18    )
19    .unwrap();
20    writeln!(
21        xml,
22        r#"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
23    )
24    .unwrap();
25    writeln!(
26        xml,
27        r#"         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
28    )
29    .unwrap();
30
31    // Key definitions for node attributes
32    writeln!(
33        xml,
34        r#"  <key id="label" for="node" attr.name="label" attr.type="string"/>"#
35    )
36    .unwrap();
37    writeln!(
38        xml,
39        r#"  <key id="node_type" for="node" attr.name="node_type" attr.type="string"/>"#
40    )
41    .unwrap();
42    writeln!(
43        xml,
44        r#"  <key id="source_file" for="node" attr.name="source_file" attr.type="string"/>"#
45    )
46    .unwrap();
47    writeln!(
48        xml,
49        r#"  <key id="community" for="node" attr.name="community" attr.type="int"/>"#
50    )
51    .unwrap();
52
53    // Key definitions for edge attributes
54    writeln!(
55        xml,
56        r#"  <key id="relation" for="edge" attr.name="relation" attr.type="string"/>"#
57    )
58    .unwrap();
59    writeln!(
60        xml,
61        r#"  <key id="confidence" for="edge" attr.name="confidence" attr.type="string"/>"#
62    )
63    .unwrap();
64    writeln!(
65        xml,
66        r#"  <key id="confidence_score" for="edge" attr.name="confidence_score" attr.type="double"/>"#
67    )
68    .unwrap();
69    writeln!(
70        xml,
71        r#"  <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
72    )
73    .unwrap();
74
75    writeln!(xml, r#"  <graph id="G" edgedefault="undirected">"#).unwrap();
76
77    // Nodes
78    for node in graph.nodes() {
79        writeln!(xml, r#"    <node id="{}">"#, xml_escape(&node.id)).unwrap();
80        writeln!(
81            xml,
82            r#"      <data key="label">{}</data>"#,
83            xml_escape(&node.label)
84        )
85        .unwrap();
86        writeln!(
87            xml,
88            r#"      <data key="node_type">{:?}</data>"#,
89            node.node_type
90        )
91        .unwrap();
92        writeln!(
93            xml,
94            r#"      <data key="source_file">{}</data>"#,
95            xml_escape(&node.source_file)
96        )
97        .unwrap();
98        if let Some(c) = node.community {
99            writeln!(xml, r#"      <data key="community">{}</data>"#, c).unwrap();
100        }
101        writeln!(xml, "    </node>").unwrap();
102    }
103
104    // Edges
105    for (i, edge) in graph.edges().iter().enumerate() {
106        writeln!(
107            xml,
108            r#"    <edge id="e{}" source="{}" target="{}">"#,
109            i,
110            xml_escape(&edge.source),
111            xml_escape(&edge.target)
112        )
113        .unwrap();
114        writeln!(
115            xml,
116            r#"      <data key="relation">{}</data>"#,
117            xml_escape(&edge.relation)
118        )
119        .unwrap();
120        writeln!(
121            xml,
122            r#"      <data key="confidence">{:?}</data>"#,
123            edge.confidence
124        )
125        .unwrap();
126        writeln!(
127            xml,
128            r#"      <data key="confidence_score">{}</data>"#,
129            edge.confidence_score
130        )
131        .unwrap();
132        writeln!(xml, r#"      <data key="weight">{}</data>"#, edge.weight).unwrap();
133        writeln!(xml, "    </edge>").unwrap();
134    }
135
136    writeln!(xml, "  </graph>").unwrap();
137    writeln!(xml, "</graphml>").unwrap();
138
139    fs::create_dir_all(output_dir)?;
140    let path = output_dir.join("graph.graphml");
141    fs::write(&path, &xml)?;
142    info!(path = %path.display(), "exported GraphML");
143    Ok(path)
144}
145
146fn xml_escape(s: &str) -> String {
147    s.replace('&', "&amp;")
148        .replace('<', "&lt;")
149        .replace('>', "&gt;")
150        .replace('"', "&quot;")
151        .replace('\'', "&apos;")
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use graphify_core::confidence::Confidence;
158    use graphify_core::graph::KnowledgeGraph;
159    use graphify_core::model::{GraphEdge, GraphNode, NodeType};
160    use std::collections::HashMap;
161
162    fn sample_graph() -> KnowledgeGraph {
163        let mut kg = KnowledgeGraph::new();
164        kg.add_node(GraphNode {
165            id: "a".into(),
166            label: "Node A".into(),
167            source_file: "test.rs".into(),
168            source_location: None,
169            node_type: NodeType::Class,
170            community: Some(0),
171            extra: HashMap::new(),
172        })
173        .unwrap();
174        kg.add_node(GraphNode {
175            id: "b".into(),
176            label: "Node B".into(),
177            source_file: "test.rs".into(),
178            source_location: None,
179            node_type: NodeType::Function,
180            community: None,
181            extra: HashMap::new(),
182        })
183        .unwrap();
184        kg.add_edge(GraphEdge {
185            source: "a".into(),
186            target: "b".into(),
187            relation: "calls".into(),
188            confidence: Confidence::Extracted,
189            confidence_score: 1.0,
190            source_file: "test.rs".into(),
191            source_location: None,
192            weight: 1.0,
193            extra: HashMap::new(),
194        })
195        .unwrap();
196        kg
197    }
198
199    #[test]
200    fn export_graphml_creates_valid_xml() {
201        let dir = tempfile::tempdir().unwrap();
202        let kg = sample_graph();
203        let path = export_graphml(&kg, dir.path()).unwrap();
204        assert!(path.exists());
205
206        let content = std::fs::read_to_string(&path).unwrap();
207        assert!(content.contains("<graphml"));
208        assert!(content.contains(r#"<node id="a">"#));
209        assert!(content.contains(r#"<node id="b">"#));
210        assert!(content.contains(r#"source="a""#));
211        assert!(content.contains("</graphml>"));
212    }
213
214    #[test]
215    fn xml_escape_special_chars() {
216        assert_eq!(xml_escape("<a&b>"), "&lt;a&amp;b&gt;");
217    }
218}