Skip to main content

rust_igraph/algorithms/io/
graphml.rs

1//! `GraphML` writer (ALGO-IO-011).
2//!
3//! Writes graphs in the `GraphML` XML format. This is a write-only
4//! implementation since reading `GraphML` requires a full XML parser.
5//!
6//! ```text
7//! <?xml version="1.0" encoding="UTF-8"?>
8//! <graphml xmlns="http://graphml.graphdrawing.org/xmlns">
9//!   <graph id="G" edgedefault="undirected">
10//!     <node id="n0"/>
11//!     <node id="n1"/>
12//!     <edge source="n0" target="n1"/>
13//!   </graph>
14//! </graphml>
15//! ```
16//!
17//! Counterpart of `igraph_write_graph_graphml`.
18
19use std::io::Write;
20
21use crate::core::{Graph, IgraphError, IgraphResult};
22
23/// Write a graph in `GraphML` format.
24///
25/// Outputs valid `GraphML` XML with node and edge elements. If `labels`
26/// is provided, uses them as node IDs; otherwise uses `n0`, `n1`, etc.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, write_graphml};
32///
33/// let mut g = Graph::with_vertices(3);
34/// g.add_edge(0, 1).unwrap();
35/// g.add_edge(1, 2).unwrap();
36///
37/// let mut buf = Vec::new();
38/// write_graphml(&g, None, &mut buf).unwrap();
39/// let s = String::from_utf8(buf).unwrap();
40/// assert!(s.contains("<graphml"));
41/// assert!(s.contains("edgedefault=\"undirected\""));
42/// assert!(s.contains("<node id=\"n0\""));
43/// ```
44pub 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    // Nodes
73    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    // Edges
79    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("&amp;"),
110            '<' => out.push_str("&lt;"),
111            '>' => out.push_str("&gt;"),
112            '"' => out.push_str("&quot;"),
113            '\'' => out.push_str("&apos;"),
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&amp;B\"/>"));
182        assert!(s.contains("<node id=\"C&lt;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}