use std::io::Write;
use crate::core::{Graph, IgraphError, IgraphResult};
pub fn write_graphml<W: Write>(
graph: &Graph,
labels: Option<&[String]>,
writer: &mut W,
) -> IgraphResult<()> {
if let Some(l) = labels {
if l.len() != graph.vcount() as usize {
return Err(IgraphError::InvalidArgument(format!(
"labels length {} does not match vcount {}",
l.len(),
graph.vcount()
)));
}
}
let edge_default = if graph.is_directed() {
"directed"
} else {
"undirected"
};
writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
writeln!(
writer,
"<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\">"
)?;
writeln!(writer, " <graph id=\"G\" edgedefault=\"{edge_default}\">")?;
for v in 0..graph.vcount() {
let node_id = vertex_id(v, labels);
writeln!(writer, " <node id=\"{}\"/>", xml_escape(&node_id))?;
}
for eid in 0..graph.ecount() {
#[allow(clippy::cast_possible_truncation)]
let (from, to) = graph.edge(eid as u32)?;
let src_id = vertex_id(from, labels);
let tgt_id = vertex_id(to, labels);
writeln!(
writer,
" <edge source=\"{}\" target=\"{}\"/>",
xml_escape(&src_id),
xml_escape(&tgt_id)
)?;
}
writeln!(writer, " </graph>")?;
writeln!(writer, "</graphml>")?;
Ok(())
}
fn vertex_id(v: u32, labels: Option<&[String]>) -> String {
match labels {
Some(l) => l[v as usize].clone(),
None => format!("n{v}"),
}
}
fn xml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_undirected() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let mut buf = Vec::new();
write_graphml(&g, None, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<?xml version=\"1.0\""));
assert!(s.contains("edgedefault=\"undirected\""));
assert!(s.contains("<node id=\"n0\"/>"));
assert!(s.contains("<node id=\"n1\"/>"));
assert!(s.contains("<node id=\"n2\"/>"));
assert!(s.contains("<edge source=\"n0\" target=\"n1\"/>"));
assert!(s.contains("<edge source=\"n1\" target=\"n2\"/>"));
assert!(s.contains("</graphml>"));
}
#[test]
fn test_directed() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
let mut buf = Vec::new();
write_graphml(&g, None, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("edgedefault=\"directed\""));
}
#[test]
fn test_with_labels() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let labels = vec!["Alice".to_string(), "Bob".to_string()];
let mut buf = Vec::new();
write_graphml(&g, Some(&labels), &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<node id=\"Alice\"/>"));
assert!(s.contains("<node id=\"Bob\"/>"));
assert!(s.contains("<edge source=\"Alice\" target=\"Bob\"/>"));
}
#[test]
fn test_xml_escaping() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let labels = vec!["A&B".to_string(), "C<D".to_string()];
let mut buf = Vec::new();
write_graphml(&g, Some(&labels), &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<node id=\"A&B\"/>"));
assert!(s.contains("<node id=\"C<D\"/>"));
}
#[test]
fn test_empty_graph() {
let g = Graph::with_vertices(0);
let mut buf = Vec::new();
write_graphml(&g, None, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<graph id=\"G\""));
assert!(s.contains("</graph>"));
}
#[test]
fn test_no_edges() {
let g = Graph::with_vertices(3);
let mut buf = Vec::new();
write_graphml(&g, None, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<node id=\"n0\"/>"));
assert!(s.contains("<node id=\"n1\"/>"));
assert!(s.contains("<node id=\"n2\"/>"));
assert!(!s.contains("<edge"));
}
#[test]
fn test_self_loop() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
let mut buf = Vec::new();
write_graphml(&g, None, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("<edge source=\"n0\" target=\"n0\"/>"));
}
#[test]
fn test_labels_mismatch_error() {
let g = Graph::with_vertices(3);
let labels = vec!["A".to_string()];
let mut buf = Vec::new();
assert!(write_graphml(&g, Some(&labels), &mut buf).is_err());
}
}