rust_igraph/algorithms/io/
graphml.rs1use std::io::Write;
20
21use crate::core::{Graph, IgraphError, IgraphResult};
22
23pub fn write_graphml<W: Write>(
45 graph: &Graph,
46 labels: Option<&[String]>,
47 writer: &mut W,
48) -> IgraphResult<()> {
49 if let Some(l) = labels {
50 if l.len() != graph.vcount() as usize {
51 return Err(IgraphError::InvalidArgument(format!(
52 "labels length {} does not match vcount {}",
53 l.len(),
54 graph.vcount()
55 )));
56 }
57 }
58
59 let edge_default = if graph.is_directed() {
60 "directed"
61 } else {
62 "undirected"
63 };
64
65 writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
66 writeln!(
67 writer,
68 "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\">"
69 )?;
70 writeln!(writer, " <graph id=\"G\" edgedefault=\"{edge_default}\">")?;
71
72 for v in 0..graph.vcount() {
74 let node_id = vertex_id(v, labels);
75 writeln!(writer, " <node id=\"{}\"/>", xml_escape(&node_id))?;
76 }
77
78 for eid in 0..graph.ecount() {
80 #[allow(clippy::cast_possible_truncation)]
81 let (from, to) = graph.edge(eid as u32)?;
82 let src_id = vertex_id(from, labels);
83 let tgt_id = vertex_id(to, labels);
84 writeln!(
85 writer,
86 " <edge source=\"{}\" target=\"{}\"/>",
87 xml_escape(&src_id),
88 xml_escape(&tgt_id)
89 )?;
90 }
91
92 writeln!(writer, " </graph>")?;
93 writeln!(writer, "</graphml>")?;
94
95 Ok(())
96}
97
98fn vertex_id(v: u32, labels: Option<&[String]>) -> String {
99 match labels {
100 Some(l) => l[v as usize].clone(),
101 None => format!("n{v}"),
102 }
103}
104
105fn xml_escape(s: &str) -> String {
106 let mut out = String::with_capacity(s.len());
107 for c in s.chars() {
108 match c {
109 '&' => out.push_str("&"),
110 '<' => out.push_str("<"),
111 '>' => out.push_str(">"),
112 '"' => out.push_str("""),
113 '\'' => out.push_str("'"),
114 _ => out.push(c),
115 }
116 }
117 out
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_basic_undirected() {
126 let mut g = Graph::with_vertices(3);
127 g.add_edge(0, 1).unwrap();
128 g.add_edge(1, 2).unwrap();
129
130 let mut buf = Vec::new();
131 write_graphml(&g, None, &mut buf).unwrap();
132 let s = String::from_utf8(buf).unwrap();
133
134 assert!(s.contains("<?xml version=\"1.0\""));
135 assert!(s.contains("edgedefault=\"undirected\""));
136 assert!(s.contains("<node id=\"n0\"/>"));
137 assert!(s.contains("<node id=\"n1\"/>"));
138 assert!(s.contains("<node id=\"n2\"/>"));
139 assert!(s.contains("<edge source=\"n0\" target=\"n1\"/>"));
140 assert!(s.contains("<edge source=\"n1\" target=\"n2\"/>"));
141 assert!(s.contains("</graphml>"));
142 }
143
144 #[test]
145 fn test_directed() {
146 let mut g = Graph::new(2, true).unwrap();
147 g.add_edge(0, 1).unwrap();
148
149 let mut buf = Vec::new();
150 write_graphml(&g, None, &mut buf).unwrap();
151 let s = String::from_utf8(buf).unwrap();
152
153 assert!(s.contains("edgedefault=\"directed\""));
154 }
155
156 #[test]
157 fn test_with_labels() {
158 let mut g = Graph::with_vertices(2);
159 g.add_edge(0, 1).unwrap();
160
161 let labels = vec!["Alice".to_string(), "Bob".to_string()];
162 let mut buf = Vec::new();
163 write_graphml(&g, Some(&labels), &mut buf).unwrap();
164 let s = String::from_utf8(buf).unwrap();
165
166 assert!(s.contains("<node id=\"Alice\"/>"));
167 assert!(s.contains("<node id=\"Bob\"/>"));
168 assert!(s.contains("<edge source=\"Alice\" target=\"Bob\"/>"));
169 }
170
171 #[test]
172 fn test_xml_escaping() {
173 let mut g = Graph::with_vertices(2);
174 g.add_edge(0, 1).unwrap();
175
176 let labels = vec!["A&B".to_string(), "C<D".to_string()];
177 let mut buf = Vec::new();
178 write_graphml(&g, Some(&labels), &mut buf).unwrap();
179 let s = String::from_utf8(buf).unwrap();
180
181 assert!(s.contains("<node id=\"A&B\"/>"));
182 assert!(s.contains("<node id=\"C<D\"/>"));
183 }
184
185 #[test]
186 fn test_empty_graph() {
187 let g = Graph::with_vertices(0);
188
189 let mut buf = Vec::new();
190 write_graphml(&g, None, &mut buf).unwrap();
191 let s = String::from_utf8(buf).unwrap();
192
193 assert!(s.contains("<graph id=\"G\""));
194 assert!(s.contains("</graph>"));
195 }
196
197 #[test]
198 fn test_no_edges() {
199 let g = Graph::with_vertices(3);
200
201 let mut buf = Vec::new();
202 write_graphml(&g, None, &mut buf).unwrap();
203 let s = String::from_utf8(buf).unwrap();
204
205 assert!(s.contains("<node id=\"n0\"/>"));
206 assert!(s.contains("<node id=\"n1\"/>"));
207 assert!(s.contains("<node id=\"n2\"/>"));
208 assert!(!s.contains("<edge"));
209 }
210
211 #[test]
212 fn test_self_loop() {
213 let mut g = Graph::with_vertices(2);
214 g.add_edge(0, 0).unwrap();
215
216 let mut buf = Vec::new();
217 write_graphml(&g, None, &mut buf).unwrap();
218 let s = String::from_utf8(buf).unwrap();
219
220 assert!(s.contains("<edge source=\"n0\" target=\"n0\"/>"));
221 }
222
223 #[test]
224 fn test_labels_mismatch_error() {
225 let g = Graph::with_vertices(3);
226 let labels = vec!["A".to_string()];
227 let mut buf = Vec::new();
228 assert!(write_graphml(&g, Some(&labels), &mut buf).is_err());
229 }
230}