use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use graphify_core::graph::KnowledgeGraph;
use tracing::info;
pub fn export_graphml(graph: &KnowledgeGraph, output_dir: &Path) -> anyhow::Result<PathBuf> {
let mut xml = String::with_capacity(4096);
writeln!(xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
xml,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
)?;
writeln!(
xml,
r#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
)?;
writeln!(
xml,
r#" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
)?;
writeln!(
xml,
r#" <key id="label" for="node" attr.name="label" attr.type="string"/>"#
)?;
writeln!(
xml,
r#" <key id="node_type" for="node" attr.name="node_type" attr.type="string"/>"#
)?;
writeln!(
xml,
r#" <key id="source_file" for="node" attr.name="source_file" attr.type="string"/>"#
)?;
writeln!(
xml,
r#" <key id="community" for="node" attr.name="community" attr.type="int"/>"#
)?;
writeln!(
xml,
r#" <key id="relation" for="edge" attr.name="relation" attr.type="string"/>"#
)?;
writeln!(
xml,
r#" <key id="confidence" for="edge" attr.name="confidence" attr.type="string"/>"#
)?;
writeln!(
xml,
r#" <key id="confidence_score" for="edge" attr.name="confidence_score" attr.type="double"/>"#
)?;
writeln!(
xml,
r#" <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
)?;
writeln!(xml, r#" <graph id="G" edgedefault="undirected">"#)?;
for node in graph.nodes() {
writeln!(xml, r#" <node id="{}">"#, xml_escape(&node.id))?;
writeln!(
xml,
r#" <data key="label">{}</data>"#,
xml_escape(&node.label)
)?;
writeln!(
xml,
r#" <data key="node_type">{}</data>"#,
node.node_type
)?;
writeln!(
xml,
r#" <data key="source_file">{}</data>"#,
xml_escape(&node.source_file)
)?;
if let Some(c) = node.community {
writeln!(xml, r#" <data key="community">{c}</data>"#)?;
}
writeln!(xml, " </node>")?;
}
for (i, edge) in graph.edges().iter().enumerate() {
writeln!(
xml,
r#" <edge id="e{}" source="{}" target="{}">"#,
i,
xml_escape(&edge.source),
xml_escape(&edge.target)
)?;
writeln!(
xml,
r#" <data key="relation">{}</data>"#,
xml_escape(&edge.relation)
)?;
writeln!(
xml,
r#" <data key="confidence">{}</data>"#,
edge.confidence
)?;
writeln!(
xml,
r#" <data key="confidence_score">{}</data>"#,
edge.confidence_score
)?;
writeln!(xml, r#" <data key="weight">{}</data>"#, edge.weight)?;
writeln!(xml, " </edge>")?;
}
writeln!(xml, " </graph>")?;
writeln!(xml, "</graphml>")?;
fs::create_dir_all(output_dir)?;
let path = output_dir.join("graph.graphml");
fs::write(&path, &xml)?;
info!(path = %path.display(), "exported GraphML");
Ok(path)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use graphify_core::confidence::Confidence;
use graphify_core::graph::KnowledgeGraph;
use graphify_core::model::{GraphEdge, GraphNode, NodeType};
use std::collections::HashMap;
fn sample_graph() -> KnowledgeGraph {
let mut kg = KnowledgeGraph::new();
kg.add_node(GraphNode {
id: "a".into(),
label: "Node A".into(),
source_file: "test.rs".into(),
source_location: None,
node_type: NodeType::Class,
community: Some(0),
extra: HashMap::new(),
})
.unwrap();
kg.add_node(GraphNode {
id: "b".into(),
label: "Node B".into(),
source_file: "test.rs".into(),
source_location: None,
node_type: NodeType::Function,
community: None,
extra: HashMap::new(),
})
.unwrap();
kg.add_edge(GraphEdge {
source: "a".into(),
target: "b".into(),
relation: "calls".into(),
confidence: Confidence::Extracted,
confidence_score: 1.0,
source_file: "test.rs".into(),
source_location: None,
weight: 1.0,
extra: HashMap::new(),
})
.unwrap();
kg
}
#[test]
fn export_graphml_creates_valid_xml() {
let dir = tempfile::tempdir().unwrap();
let kg = sample_graph();
let path = export_graphml(&kg, dir.path()).unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("<graphml"));
assert!(content.contains(r#"<node id="a">"#));
assert!(content.contains(r#"<node id="b">"#));
assert!(content.contains(r#"source="a""#));
assert!(content.contains("</graphml>"));
}
#[test]
fn xml_escape_special_chars() {
assert_eq!(xml_escape("<a&b>"), "<a&b>");
}
}