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