Skip to main content

rust_igraph/algorithms/io/
graphml.rs

1//! `GraphML` reader / writer (ALGO-IO-011, ALGO-IO-012).
2//!
3//! Reads and writes graphs in the `GraphML` XML format. The reader uses a
4//! lightweight custom XML parser (no external dependencies) that handles
5//! both structural elements (`<node>`, `<edge>`) and attribute data
6//! (`<key>` definitions and `<data>` elements).
7//!
8//! Counterparts of `igraph_read_graph_graphml` and `igraph_write_graph_graphml`.
9
10use std::collections::HashMap;
11use std::io::{BufRead, BufReader, Read, Write};
12
13use crate::core::attributes::AttributeValue;
14use crate::core::{Graph, IgraphError, IgraphResult};
15
16/// Result of parsing a `GraphML` file.
17pub struct GraphmlGraph {
18    /// The parsed graph (with attributes populated).
19    pub graph: Graph,
20    /// Node labels (the `id` attributes from the `GraphML` `<node>` elements),
21    /// in vertex-index order.
22    pub labels: Vec<String>,
23}
24
25#[derive(Debug, Clone)]
26struct KeyDef {
27    attr_name: String,
28    attr_type: String,
29}
30
31fn parse_attr_value(raw: &str, attr_type: &str) -> AttributeValue {
32    match attr_type {
33        "int" | "long" | "float" | "double" => {
34            if let Ok(v) = raw.parse::<f64>() {
35                AttributeValue::Numeric(v)
36            } else {
37                AttributeValue::String(raw.to_owned())
38            }
39        }
40        "boolean" => {
41            let v = matches!(raw, "true" | "1" | "yes");
42            AttributeValue::Boolean(v)
43        }
44        _ => AttributeValue::String(raw.to_owned()),
45    }
46}
47
48/// Read a graph from `GraphML` format.
49///
50/// Parses the first `<graph>` element in the input. Extracts structural
51/// elements (`<node>`, `<edge>`) and attribute data (`<key>` definitions
52/// and `<data>` elements). Attributes are stored on the returned graph
53/// using the [`Graph`] attribute system.
54///
55/// # Examples
56///
57/// ```
58/// use rust_igraph::{Graph, read_graphml, write_graphml, AttributeValue};
59///
60/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
61/// <graphml xmlns="http://graphml.graphdrawing.org/xmlns">
62///   <key id="d0" for="node" attr.name="label" attr.type="string"/>
63///   <key id="d1" for="edge" attr.name="weight" attr.type="double"/>
64///   <graph id="G" edgedefault="undirected">
65///     <node id="Alice"><data key="d0">Alice</data></node>
66///     <node id="Bob"><data key="d0">Bob</data></node>
67///     <edge source="Alice" target="Bob"><data key="d1">1.5</data></edge>
68///   </graph>
69/// </graphml>"#;
70///
71/// let result = read_graphml(xml.as_bytes()).unwrap();
72/// assert_eq!(result.graph.vcount(), 2);
73/// assert_eq!(result.graph.ecount(), 1);
74/// assert_eq!(
75///     result.graph.vertex_attribute("label", 0).and_then(|v| v.as_str()),
76///     Some("Alice"),
77/// );
78/// assert_eq!(
79///     result.graph.edge_attribute("weight", 0).and_then(|v| v.as_f64()),
80///     Some(1.5),
81/// );
82/// ```
83#[allow(clippy::too_many_lines)]
84pub fn read_graphml<R: Read>(input: R) -> IgraphResult<GraphmlGraph> {
85    let reader = BufReader::new(input);
86    let mut content = String::new();
87    for line in reader.lines() {
88        content.push_str(&line?);
89        content.push('\n');
90    }
91
92    let keys = parse_key_definitions(&content)?;
93    let graph_section = extract_graph_section(&content)?;
94    let directed = parse_edgedefault(graph_section);
95
96    let parsed = parse_nodes(graph_section)?;
97
98    let n = u32::try_from(parsed.ids.len())
99        .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
100    let mut graph = Graph::new(n, directed)?;
101
102    if !parsed.ids.is_empty() {
103        let label_vals: Vec<AttributeValue> = parsed
104            .ids
105            .iter()
106            .map(|s| AttributeValue::String(s.clone()))
107            .collect();
108        graph.set_vertex_attribute_all("name", label_vals)?;
109    }
110
111    apply_node_attributes(&mut graph, &parsed.data, &keys)?;
112
113    let edge_data_list = parse_and_add_edges(&mut graph, graph_section, &parsed.map)?;
114
115    apply_edge_attributes(&mut graph, &edge_data_list, &keys)?;
116    apply_graph_attributes(&mut graph, graph_section, &keys);
117
118    Ok(GraphmlGraph {
119        graph,
120        labels: parsed.ids,
121    })
122}
123
124fn parse_key_definitions(content: &str) -> IgraphResult<HashMap<String, KeyDef>> {
125    let mut keys: HashMap<String, KeyDef> = HashMap::new();
126    let mut pos = 0;
127    while let Some(key_start) = content[pos..].find("<key") {
128        let abs_start = pos + key_start;
129        let after = abs_start + 4;
130        match content.as_bytes().get(after) {
131            Some(b' ' | b'>' | b'/' | b'\t' | b'\n' | b'\r') | None => {}
132            _ => {
133                pos = abs_start + 1;
134                continue;
135            }
136        }
137        let tag_end = find_tag_end(&content[abs_start..]).ok_or_else(|| IgraphError::Parse {
138            line: 0,
139            message: "unclosed <key> tag".to_owned(),
140        })?;
141        let tag = &content[abs_start..abs_start + tag_end];
142        if let Some(id) = extract_attr(tag, "id") {
143            let attr_name = extract_attr(tag, "attr.name").unwrap_or_else(|| id.clone());
144            let attr_type = extract_attr(tag, "attr.type").unwrap_or_else(|| "string".to_owned());
145            keys.insert(
146                id,
147                KeyDef {
148                    attr_name,
149                    attr_type,
150                },
151            );
152        }
153        pos = abs_start + tag_end;
154    }
155    Ok(keys)
156}
157
158fn extract_graph_section(content: &str) -> IgraphResult<&str> {
159    let graph_start = find_graph_element(content).ok_or_else(|| IgraphError::Parse {
160        line: 0,
161        message: "no <graph> element found".to_owned(),
162    })?;
163    let graph_end = content[graph_start..]
164        .find("</graph>")
165        .or_else(|| content[graph_start..].find("/>"))
166        .ok_or_else(|| IgraphError::Parse {
167            line: 0,
168            message: "unclosed <graph> element".to_owned(),
169        })?;
170    Ok(&content[graph_start..graph_start + graph_end + 8])
171}
172
173struct ParsedNodes {
174    ids: Vec<String>,
175    map: HashMap<String, u32>,
176    data: Vec<Vec<(String, String)>>,
177}
178
179fn parse_nodes(graph_section: &str) -> IgraphResult<ParsedNodes> {
180    let mut node_ids: Vec<String> = Vec::new();
181    let mut node_map: HashMap<String, u32> = HashMap::new();
182    let mut node_data: Vec<Vec<(String, String)>> = Vec::new();
183
184    let mut pos = 0;
185    while let Some(node_start) = graph_section[pos..].find("<node") {
186        let abs_start = pos + node_start;
187        let after = abs_start + 5;
188        match graph_section.as_bytes().get(after) {
189            Some(b' ' | b'>' | b'/' | b'\t' | b'\n' | b'\r') | None => {}
190            _ => {
191                pos = abs_start + 1;
192                continue;
193            }
194        }
195
196        let tag_end_pos =
197            find_tag_end(&graph_section[abs_start..]).ok_or_else(|| IgraphError::Parse {
198                line: 0,
199                message: "unclosed <node> tag".to_owned(),
200            })?;
201        let tag = &graph_section[abs_start..abs_start + tag_end_pos];
202
203        if let Some(id) = extract_attr(tag, "id") {
204            let idx = u32::try_from(node_ids.len())
205                .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
206            node_map.insert(id.clone(), idx);
207            node_ids.push(id);
208
209            let node_block_end;
210            if tag.ends_with("/>") {
211                node_block_end = abs_start + tag_end_pos;
212                node_data.push(Vec::new());
213            } else if let Some(close) = graph_section[abs_start..].find("</node>") {
214                node_block_end = abs_start + close + 7;
215                let block = &graph_section[abs_start + tag_end_pos..abs_start + close];
216                node_data.push(extract_data_elements(block));
217            } else {
218                node_block_end = abs_start + tag_end_pos;
219                node_data.push(Vec::new());
220            }
221            pos = node_block_end;
222        } else {
223            pos = abs_start + tag_end_pos;
224        }
225    }
226    Ok(ParsedNodes {
227        ids: node_ids,
228        map: node_map,
229        data: node_data,
230    })
231}
232
233fn apply_node_attributes(
234    graph: &mut Graph,
235    node_data: &[Vec<(String, String)>],
236    keys: &HashMap<String, KeyDef>,
237) -> IgraphResult<()> {
238    for (vid, data_items) in node_data.iter().enumerate() {
239        for (key_id, raw_val) in data_items {
240            if let Some(key_def) = keys.get(key_id) {
241                let value = parse_attr_value(raw_val, &key_def.attr_type);
242                let vid_u32 =
243                    u32::try_from(vid).map_err(|_| IgraphError::Internal("vertex id overflow"))?;
244                graph.set_vertex_attribute(&key_def.attr_name, vid_u32, value)?;
245            }
246        }
247    }
248    Ok(())
249}
250
251fn parse_and_add_edges(
252    graph: &mut Graph,
253    graph_section: &str,
254    node_map: &HashMap<String, u32>,
255) -> IgraphResult<Vec<Vec<(String, String)>>> {
256    let mut edge_data_list: Vec<Vec<(String, String)>> = Vec::new();
257    let mut pos = 0;
258    while let Some(edge_start) = graph_section[pos..].find("<edge") {
259        let abs_start = pos + edge_start;
260        let tag_end_pos =
261            find_tag_end(&graph_section[abs_start..]).ok_or_else(|| IgraphError::Parse {
262                line: 0,
263                message: "unclosed <edge> tag".to_owned(),
264            })?;
265        let tag = &graph_section[abs_start..abs_start + tag_end_pos];
266        let source = extract_attr(tag, "source").ok_or_else(|| IgraphError::Parse {
267            line: 0,
268            message: "edge missing source attribute".to_owned(),
269        })?;
270        let target = extract_attr(tag, "target").ok_or_else(|| IgraphError::Parse {
271            line: 0,
272            message: "edge missing target attribute".to_owned(),
273        })?;
274
275        let src_idx = *node_map.get(&source).ok_or_else(|| IgraphError::Parse {
276            line: 0,
277            message: format!("edge references unknown node \"{source}\""),
278        })?;
279        let tgt_idx = *node_map.get(&target).ok_or_else(|| IgraphError::Parse {
280            line: 0,
281            message: format!("edge references unknown node \"{target}\""),
282        })?;
283        graph.add_edge(src_idx, tgt_idx)?;
284
285        if tag.ends_with("/>") {
286            edge_data_list.push(Vec::new());
287        } else if let Some(close) = graph_section[abs_start..].find("</edge>") {
288            let block = &graph_section[abs_start + tag_end_pos..abs_start + close];
289            edge_data_list.push(extract_data_elements(block));
290            pos = abs_start + close + 7;
291            continue;
292        } else {
293            edge_data_list.push(Vec::new());
294        }
295        pos = abs_start + tag_end_pos;
296    }
297    Ok(edge_data_list)
298}
299
300fn apply_edge_attributes(
301    graph: &mut Graph,
302    edge_data_list: &[Vec<(String, String)>],
303    keys: &HashMap<String, KeyDef>,
304) -> IgraphResult<()> {
305    for (eid, data_items) in edge_data_list.iter().enumerate() {
306        for (key_id, raw_val) in data_items {
307            if let Some(key_def) = keys.get(key_id) {
308                let value = parse_attr_value(raw_val, &key_def.attr_type);
309                let eid_u32 =
310                    u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
311                graph.set_edge_attribute(&key_def.attr_name, eid_u32, value)?;
312            }
313        }
314    }
315    Ok(())
316}
317
318fn apply_graph_attributes(graph: &mut Graph, graph_section: &str, keys: &HashMap<String, KeyDef>) {
319    let graph_tag_end = find_tag_end(graph_section).unwrap_or(graph_section.len());
320    let graph_inner = &graph_section[graph_tag_end..];
321    let first_child = graph_inner
322        .find("<node")
323        .unwrap_or(graph_inner.len())
324        .min(graph_inner.find("<edge").unwrap_or(graph_inner.len()));
325    let preamble = &graph_inner[..first_child];
326    for (key_id, raw_val) in extract_data_elements(preamble) {
327        if let Some(key_def) = keys.get(&key_id) {
328            let value = parse_attr_value(&raw_val, &key_def.attr_type);
329            graph.set_graph_attribute(&key_def.attr_name, value);
330        }
331    }
332}
333
334fn extract_data_elements(block: &str) -> Vec<(String, String)> {
335    let mut result = Vec::new();
336    let mut pos = 0;
337    while let Some(data_start) = block[pos..].find("<data") {
338        let abs_start = pos + data_start;
339        let Some(tag_end) = find_tag_end(&block[abs_start..]) else {
340            break;
341        };
342        let tag = &block[abs_start..abs_start + tag_end];
343
344        if let Some(key_id) = extract_attr(tag, "key") {
345            if tag.ends_with("/>") {
346                result.push((key_id, String::new()));
347                pos = abs_start + tag_end;
348            } else if let Some(close) = block[abs_start..].find("</data>") {
349                let content = &block[abs_start + tag_end..abs_start + close];
350                let unescaped = xml_unescape(content.trim());
351                result.push((key_id, unescaped));
352                pos = abs_start + close + 7;
353            } else {
354                pos = abs_start + tag_end;
355            }
356        } else {
357            pos = abs_start + tag_end;
358        }
359    }
360    result
361}
362
363/// Find the start of a `<graph` element that is NOT `<graphml`.
364fn find_graph_element(s: &str) -> Option<usize> {
365    let mut search_from = 0;
366    while let Some(pos) = s[search_from..].find("<graph") {
367        let abs = search_from + pos;
368        let after = abs + 6;
369        match s.as_bytes().get(after) {
370            Some(b' ' | b'>' | b'/' | b'\t' | b'\n' | b'\r') | None => return Some(abs),
371            _ => search_from = abs + 1,
372        }
373    }
374    None
375}
376
377fn parse_edgedefault(graph_tag: &str) -> bool {
378    if let Some(pos) = graph_tag.find("edgedefault") {
379        let rest = &graph_tag[pos..];
380        if let Some(q1) = rest.find('"') {
381            let after_q = &rest[q1 + 1..];
382            if let Some(q2) = after_q.find('"') {
383                let val = &after_q[..q2];
384                return val == "directed";
385            }
386        }
387        if let Some(q1) = rest.find('\'') {
388            let after_q = &rest[q1 + 1..];
389            if let Some(q2) = after_q.find('\'') {
390                let val = &after_q[..q2];
391                return val == "directed";
392            }
393        }
394    }
395    false
396}
397
398fn find_tag_end(s: &str) -> Option<usize> {
399    s.find('>').map(|p| p + 1)
400}
401
402fn extract_attr(tag: &str, attr_name: &str) -> Option<String> {
403    let needle = format!("{attr_name}=");
404    let pos = tag.find(&needle)?;
405    let rest = &tag[pos + needle.len()..];
406    let quote = rest.chars().next()?;
407    if quote != '"' && quote != '\'' {
408        return None;
409    }
410    let after = &rest[1..];
411    let end = after.find(quote)?;
412    let raw = &after[..end];
413    Some(xml_unescape(raw))
414}
415
416fn xml_unescape(s: &str) -> String {
417    s.replace("&amp;", "&")
418        .replace("&lt;", "<")
419        .replace("&gt;", ">")
420        .replace("&quot;", "\"")
421        .replace("&apos;", "'")
422}
423
424/// Write a graph in `GraphML` format, including attributes.
425///
426/// If `labels` is provided, uses them as node IDs; otherwise uses `n0`, `n1`,
427/// etc. Graph, vertex, and edge attributes from the attribute system are
428/// written as `<key>` definitions and `<data>` elements.
429///
430/// # Examples
431///
432/// ```
433/// use rust_igraph::{Graph, write_graphml, AttributeValue};
434///
435/// let mut g = Graph::with_vertices(3);
436/// g.add_edge(0, 1).unwrap();
437/// g.set_vertex_attribute("label", 0, "Alice".into()).unwrap();
438/// g.set_vertex_attribute("label", 1, "Bob".into()).unwrap();
439/// g.set_vertex_attribute("label", 2, "Carol".into()).unwrap();
440/// g.set_edge_attribute("weight", 0, 1.5.into()).unwrap();
441///
442/// let mut buf = Vec::new();
443/// write_graphml(&g, None, &mut buf).unwrap();
444/// let s = String::from_utf8(buf).unwrap();
445/// assert!(s.contains("attr.name=\"label\""));
446/// assert!(s.contains("attr.name=\"weight\""));
447/// assert!(s.contains("<data key="));
448/// ```
449#[allow(clippy::too_many_lines)]
450pub fn write_graphml<W: Write>(
451    graph: &Graph,
452    labels: Option<&[String]>,
453    writer: &mut W,
454) -> IgraphResult<()> {
455    if let Some(l) = labels {
456        if l.len() != graph.vcount() as usize {
457            return Err(IgraphError::InvalidArgument(format!(
458                "labels length {} does not match vcount {}",
459                l.len(),
460                graph.vcount()
461            )));
462        }
463    }
464
465    let edge_default = if graph.is_directed() {
466        "directed"
467    } else {
468        "undirected"
469    };
470
471    writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
472    writeln!(
473        writer,
474        "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\">"
475    )?;
476
477    let graph_attr_names = graph.graph_attribute_names();
478    let vertex_attr_names = graph.vertex_attribute_names();
479    let edge_attr_names = graph.edge_attribute_names();
480
481    let mut key_counter = 0u32;
482    let graph_key_map = write_graph_keys(graph, &graph_attr_names, &mut key_counter, writer)?;
483    let vertex_key_map = write_vertex_keys(graph, &vertex_attr_names, &mut key_counter, writer)?;
484    let edge_key_map = write_edge_keys(graph, &edge_attr_names, &mut key_counter, writer)?;
485
486    writeln!(writer, "  <graph id=\"G\" edgedefault=\"{edge_default}\">")?;
487
488    write_graph_data(graph, &graph_attr_names, &graph_key_map, writer)?;
489    write_nodes(graph, &vertex_attr_names, &vertex_key_map, labels, writer)?;
490    write_edges(graph, &edge_attr_names, &edge_key_map, labels, writer)?;
491
492    writeln!(writer, "  </graph>")?;
493    writeln!(writer, "</graphml>")?;
494
495    Ok(())
496}
497
498fn write_graph_keys<W: Write>(
499    graph: &Graph,
500    names: &[&str],
501    counter: &mut u32,
502    writer: &mut W,
503) -> IgraphResult<HashMap<String, String>> {
504    let mut map = HashMap::new();
505    for &name in names {
506        let key_id = format!("g{counter}");
507        *counter = counter.wrapping_add(1);
508        let attr_type = graph.graph_attribute(name).map_or("string", attr_type_str);
509        writeln!(
510            writer,
511            "  <key id=\"{key_id}\" for=\"graph\" attr.name=\"{}\" attr.type=\"{attr_type}\"/>",
512            xml_escape(name)
513        )?;
514        map.insert(name.to_owned(), key_id);
515    }
516    Ok(map)
517}
518
519fn write_vertex_keys<W: Write>(
520    graph: &Graph,
521    names: &[&str],
522    counter: &mut u32,
523    writer: &mut W,
524) -> IgraphResult<HashMap<String, String>> {
525    let mut map = HashMap::new();
526    for &name in names {
527        let key_id = format!("v{counter}");
528        *counter = counter.wrapping_add(1);
529        let attr_type = graph
530            .vertex_attributes(name)
531            .and_then(|v| v.first())
532            .map_or("string", attr_type_str);
533        writeln!(
534            writer,
535            "  <key id=\"{key_id}\" for=\"node\" attr.name=\"{}\" attr.type=\"{attr_type}\"/>",
536            xml_escape(name)
537        )?;
538        map.insert(name.to_owned(), key_id);
539    }
540    Ok(map)
541}
542
543fn write_edge_keys<W: Write>(
544    graph: &Graph,
545    names: &[&str],
546    counter: &mut u32,
547    writer: &mut W,
548) -> IgraphResult<HashMap<String, String>> {
549    let mut map = HashMap::new();
550    for &name in names {
551        let key_id = format!("e{counter}");
552        *counter = counter.wrapping_add(1);
553        let attr_type = graph
554            .edge_attributes(name)
555            .and_then(|v| v.first())
556            .map_or("string", attr_type_str);
557        writeln!(
558            writer,
559            "  <key id=\"{key_id}\" for=\"edge\" attr.name=\"{}\" attr.type=\"{attr_type}\"/>",
560            xml_escape(name)
561        )?;
562        map.insert(name.to_owned(), key_id);
563    }
564    Ok(map)
565}
566
567fn write_graph_data<W: Write>(
568    graph: &Graph,
569    names: &[&str],
570    key_map: &HashMap<String, String>,
571    writer: &mut W,
572) -> IgraphResult<()> {
573    for &name in names {
574        if let (Some(key_id), Some(val)) = (key_map.get(name), graph.graph_attribute(name)) {
575            writeln!(
576                writer,
577                "    <data key=\"{key_id}\">{}</data>",
578                xml_escape(&val.to_string())
579            )?;
580        }
581    }
582    Ok(())
583}
584
585fn write_nodes<W: Write>(
586    graph: &Graph,
587    attr_names: &[&str],
588    key_map: &HashMap<String, String>,
589    labels: Option<&[String]>,
590    writer: &mut W,
591) -> IgraphResult<()> {
592    for v in 0..graph.vcount() {
593        let node_id = vertex_id(v, labels);
594        let has_data = attr_names
595            .iter()
596            .any(|name| graph.vertex_attribute(name, v).is_some());
597        if has_data {
598            writeln!(writer, "    <node id=\"{}\">", xml_escape(&node_id))?;
599            for &name in attr_names {
600                if let (Some(key_id), Some(val)) =
601                    (key_map.get(name), graph.vertex_attribute(name, v))
602                {
603                    writeln!(
604                        writer,
605                        "      <data key=\"{key_id}\">{}</data>",
606                        xml_escape(&val.to_string())
607                    )?;
608                }
609            }
610            writeln!(writer, "    </node>")?;
611        } else {
612            writeln!(writer, "    <node id=\"{}\"/>", xml_escape(&node_id))?;
613        }
614    }
615    Ok(())
616}
617
618fn write_edges<W: Write>(
619    graph: &Graph,
620    attr_names: &[&str],
621    key_map: &HashMap<String, String>,
622    labels: Option<&[String]>,
623    writer: &mut W,
624) -> IgraphResult<()> {
625    for eid in 0..graph.ecount() {
626        let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
627        let (from, to) = graph.edge(eid_u32)?;
628        let src_id = vertex_id(from, labels);
629        let tgt_id = vertex_id(to, labels);
630        let has_data = attr_names
631            .iter()
632            .any(|name| graph.edge_attribute(name, eid_u32).is_some());
633        if has_data {
634            writeln!(
635                writer,
636                "    <edge source=\"{}\" target=\"{}\">",
637                xml_escape(&src_id),
638                xml_escape(&tgt_id)
639            )?;
640            for &name in attr_names {
641                if let (Some(key_id), Some(val)) =
642                    (key_map.get(name), graph.edge_attribute(name, eid_u32))
643                {
644                    writeln!(
645                        writer,
646                        "      <data key=\"{key_id}\">{}</data>",
647                        xml_escape(&val.to_string())
648                    )?;
649                }
650            }
651            writeln!(writer, "    </edge>")?;
652        } else {
653            writeln!(
654                writer,
655                "    <edge source=\"{}\" target=\"{}\"/>",
656                xml_escape(&src_id),
657                xml_escape(&tgt_id)
658            )?;
659        }
660    }
661    Ok(())
662}
663
664fn attr_type_str(val: &AttributeValue) -> &'static str {
665    match val {
666        AttributeValue::Numeric(_) => "double",
667        AttributeValue::Boolean(_) => "boolean",
668        AttributeValue::String(_) => "string",
669    }
670}
671
672fn vertex_id(v: u32, labels: Option<&[String]>) -> String {
673    match labels {
674        Some(l) => l[v as usize].clone(),
675        None => format!("n{v}"),
676    }
677}
678
679fn xml_escape(s: &str) -> String {
680    let mut out = String::with_capacity(s.len());
681    for c in s.chars() {
682        match c {
683            '&' => out.push_str("&amp;"),
684            '<' => out.push_str("&lt;"),
685            '>' => out.push_str("&gt;"),
686            '"' => out.push_str("&quot;"),
687            '\'' => out.push_str("&apos;"),
688            _ => out.push(c),
689        }
690    }
691    out
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    #[test]
699    fn test_basic_undirected() {
700        let mut g = Graph::with_vertices(3);
701        g.add_edge(0, 1).unwrap();
702        g.add_edge(1, 2).unwrap();
703
704        let mut buf = Vec::new();
705        write_graphml(&g, None, &mut buf).unwrap();
706        let s = String::from_utf8(buf).unwrap();
707
708        assert!(s.contains("<?xml version=\"1.0\""));
709        assert!(s.contains("edgedefault=\"undirected\""));
710        assert!(s.contains("n0"));
711        assert!(s.contains("n1"));
712        assert!(s.contains("n2"));
713    }
714
715    #[test]
716    fn test_directed() {
717        let mut g = Graph::new(2, true).unwrap();
718        g.add_edge(0, 1).unwrap();
719
720        let mut buf = Vec::new();
721        write_graphml(&g, None, &mut buf).unwrap();
722        let s = String::from_utf8(buf).unwrap();
723
724        assert!(s.contains("edgedefault=\"directed\""));
725    }
726
727    #[test]
728    fn test_with_labels() {
729        let mut g = Graph::with_vertices(2);
730        g.add_edge(0, 1).unwrap();
731
732        let labels = vec!["Alice".to_string(), "Bob".to_string()];
733        let mut buf = Vec::new();
734        write_graphml(&g, Some(&labels), &mut buf).unwrap();
735        let s = String::from_utf8(buf).unwrap();
736
737        assert!(s.contains("Alice"));
738        assert!(s.contains("Bob"));
739    }
740
741    #[test]
742    fn test_xml_escaping() {
743        let mut g = Graph::with_vertices(2);
744        g.add_edge(0, 1).unwrap();
745
746        let labels = vec!["A&B".to_string(), "C<D".to_string()];
747        let mut buf = Vec::new();
748        write_graphml(&g, Some(&labels), &mut buf).unwrap();
749        let s = String::from_utf8(buf).unwrap();
750
751        assert!(s.contains("A&amp;B"));
752        assert!(s.contains("C&lt;D"));
753    }
754
755    #[test]
756    fn test_empty_graph() {
757        let g = Graph::with_vertices(0);
758
759        let mut buf = Vec::new();
760        write_graphml(&g, None, &mut buf).unwrap();
761        let s = String::from_utf8(buf).unwrap();
762
763        assert!(s.contains("<graph id=\"G\""));
764        assert!(s.contains("</graph>"));
765    }
766
767    #[test]
768    fn test_self_loop() {
769        let mut g = Graph::with_vertices(2);
770        g.add_edge(0, 0).unwrap();
771
772        let mut buf = Vec::new();
773        write_graphml(&g, None, &mut buf).unwrap();
774        let s = String::from_utf8(buf).unwrap();
775
776        assert!(s.contains("source=\"n0\" target=\"n0\""));
777    }
778
779    #[test]
780    fn test_labels_mismatch_error() {
781        let g = Graph::with_vertices(3);
782        let labels = vec!["A".to_string()];
783        let mut buf = Vec::new();
784        assert!(write_graphml(&g, Some(&labels), &mut buf).is_err());
785    }
786
787    #[test]
788    fn read_basic_undirected() {
789        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
790<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
791  <graph id="G" edgedefault="undirected">
792    <node id="n0"/>
793    <node id="n1"/>
794    <node id="n2"/>
795    <edge source="n0" target="n1"/>
796    <edge source="n1" target="n2"/>
797  </graph>
798</graphml>"#;
799        let result = read_graphml(xml.as_bytes()).unwrap();
800        assert_eq!(result.graph.vcount(), 3);
801        assert_eq!(result.graph.ecount(), 2);
802        assert!(!result.graph.is_directed());
803        assert_eq!(result.labels, vec!["n0", "n1", "n2"]);
804    }
805
806    #[test]
807    fn read_directed() {
808        let xml = r#"<graphml>
809  <graph edgedefault="directed">
810    <node id="a"/>
811    <node id="b"/>
812    <edge source="a" target="b"/>
813  </graph>
814</graphml>"#;
815        let result = read_graphml(xml.as_bytes()).unwrap();
816        assert!(result.graph.is_directed());
817    }
818
819    #[test]
820    fn read_empty_graph() {
821        let xml = r#"<graphml><graph edgedefault="undirected"></graph></graphml>"#;
822        let result = read_graphml(xml.as_bytes()).unwrap();
823        assert_eq!(result.graph.vcount(), 0);
824        assert_eq!(result.graph.ecount(), 0);
825    }
826
827    #[test]
828    fn read_unknown_node_in_edge_is_error() {
829        let xml = r#"<graphml>
830  <graph edgedefault="undirected">
831    <node id="a"/>
832    <edge source="a" target="b"/>
833  </graph>
834</graphml>"#;
835        assert!(read_graphml(xml.as_bytes()).is_err());
836    }
837
838    #[test]
839    fn read_no_graph_element_is_error() {
840        let xml = r#"<graphml><key id="k"/></graphml>"#;
841        assert!(read_graphml(xml.as_bytes()).is_err());
842    }
843
844    #[test]
845    fn roundtrip_write_then_read() {
846        let mut g = Graph::new(4, true).unwrap();
847        g.add_edge(0, 1).unwrap();
848        g.add_edge(1, 2).unwrap();
849        g.add_edge(2, 3).unwrap();
850        g.add_edge(3, 0).unwrap();
851
852        let mut buf = Vec::new();
853        write_graphml(&g, None, &mut buf).unwrap();
854
855        let result = read_graphml(buf.as_slice()).unwrap();
856        assert_eq!(result.graph.vcount(), 4);
857        assert_eq!(result.graph.ecount(), 4);
858        assert!(result.graph.is_directed());
859    }
860
861    #[test]
862    fn read_with_attributes() {
863        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
864<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
865  <key id="d0" for="node" attr.name="label" attr.type="string"/>
866  <key id="d1" for="edge" attr.name="weight" attr.type="double"/>
867  <key id="d2" for="graph" attr.name="name" attr.type="string"/>
868  <graph id="G" edgedefault="undirected">
869    <data key="d2">TestGraph</data>
870    <node id="n0">
871      <data key="d0">Alice</data>
872    </node>
873    <node id="n1">
874      <data key="d0">Bob</data>
875    </node>
876    <edge source="n0" target="n1">
877      <data key="d1">2.5</data>
878    </edge>
879  </graph>
880</graphml>"#;
881        let result = read_graphml(xml.as_bytes()).unwrap();
882        assert_eq!(result.graph.vcount(), 2);
883        assert_eq!(result.graph.ecount(), 1);
884
885        assert_eq!(
886            result
887                .graph
888                .vertex_attribute("label", 0)
889                .and_then(|v| v.as_str()),
890            Some("Alice"),
891        );
892        assert_eq!(
893            result
894                .graph
895                .vertex_attribute("label", 1)
896                .and_then(|v| v.as_str()),
897            Some("Bob"),
898        );
899
900        let w = result
901            .graph
902            .edge_attribute("weight", 0)
903            .and_then(AttributeValue::as_f64);
904        assert!((w.unwrap() - 2.5).abs() < f64::EPSILON);
905
906        assert_eq!(
907            result
908                .graph
909                .graph_attribute("name")
910                .and_then(|v| v.as_str()),
911            Some("TestGraph"),
912        );
913    }
914
915    #[test]
916    fn write_with_attributes() {
917        let mut g = Graph::from_edges(&[(0, 1)], false, None).unwrap();
918        g.set_vertex_attribute("color", 0, "red".into()).unwrap();
919        g.set_vertex_attribute("color", 1, "blue".into()).unwrap();
920        g.set_edge_attribute("weight", 0, 3.0.into()).unwrap();
921        g.set_graph_attribute("title", "test".into());
922
923        let mut buf = Vec::new();
924        write_graphml(&g, None, &mut buf).unwrap();
925        let s = String::from_utf8(buf).unwrap();
926
927        assert!(s.contains("attr.name=\"color\""));
928        assert!(s.contains("attr.name=\"weight\""));
929        assert!(s.contains("attr.name=\"title\""));
930        assert!(s.contains("<data key="));
931        assert!(s.contains("red"));
932        assert!(s.contains("blue"));
933        assert!(s.contains("test"));
934    }
935
936    #[test]
937    fn roundtrip_with_attributes() {
938        let mut g = Graph::from_edges(&[(0, 1), (1, 2)], false, None).unwrap();
939        g.set_vertex_attribute("label", 0, "A".into()).unwrap();
940        g.set_vertex_attribute("label", 1, "B".into()).unwrap();
941        g.set_vertex_attribute("label", 2, "C".into()).unwrap();
942        g.set_edge_attribute("weight", 0, 1.5.into()).unwrap();
943        g.set_edge_attribute("weight", 1, 2.5.into()).unwrap();
944        g.set_graph_attribute("name", "roundtrip_test".into());
945
946        let mut buf = Vec::new();
947        write_graphml(&g, None, &mut buf).unwrap();
948
949        let result = read_graphml(buf.as_slice()).unwrap();
950        assert_eq!(result.graph.vcount(), 3);
951        assert_eq!(result.graph.ecount(), 2);
952        assert_eq!(
953            result
954                .graph
955                .vertex_attribute("label", 0)
956                .and_then(|v| v.as_str()),
957            Some("A"),
958        );
959        assert_eq!(
960            result
961                .graph
962                .graph_attribute("name")
963                .and_then(|v| v.as_str()),
964            Some("roundtrip_test"),
965        );
966    }
967
968    #[test]
969    fn read_ignores_extra_elements() {
970        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
971<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
972  <key id="d0" for="node" attr.name="label" attr.type="string"/>
973  <graph id="G" edgedefault="undirected">
974    <node id="n0">
975      <data key="d0">Alice</data>
976    </node>
977    <node id="n1">
978      <data key="d0">Bob</data>
979    </node>
980    <edge source="n0" target="n1">
981      <data key="d1">1.5</data>
982    </edge>
983  </graph>
984</graphml>"#;
985        let result = read_graphml(xml.as_bytes()).unwrap();
986        assert_eq!(result.graph.vcount(), 2);
987        assert_eq!(result.graph.ecount(), 1);
988        assert_eq!(
989            result
990                .graph
991                .vertex_attribute("label", 0)
992                .and_then(|v| v.as_str()),
993            Some("Alice"),
994        );
995    }
996
997    #[test]
998    fn read_boolean_attribute() {
999        let xml = r#"<graphml>
1000  <key id="d0" for="node" attr.name="active" attr.type="boolean"/>
1001  <graph edgedefault="undirected">
1002    <node id="n0"><data key="d0">true</data></node>
1003    <node id="n1"><data key="d0">false</data></node>
1004  </graph>
1005</graphml>"#;
1006        let result = read_graphml(xml.as_bytes()).unwrap();
1007        assert_eq!(
1008            result
1009                .graph
1010                .vertex_attribute("active", 0)
1011                .and_then(AttributeValue::as_bool),
1012            Some(true),
1013        );
1014        assert_eq!(
1015            result
1016                .graph
1017                .vertex_attribute("active", 1)
1018                .and_then(AttributeValue::as_bool),
1019            Some(false),
1020        );
1021    }
1022}