graphify_export/
graphml.rs1use std::fmt::Write;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use graphify_core::graph::KnowledgeGraph;
8use tracing::info;
9
10pub 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 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 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 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 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('&', "&")
148 .replace('<', "<")
149 .replace('>', ">")
150 .replace('"', """)
151 .replace('\'', "'")
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>"), "<a&b>");
217 }
218}