Skip to main content

rust_igraph/algorithms/io/
gml.rs

1//! GML (Graph Modelling Language) I/O (ALGO-IO-001).
2//!
3//! Reads and writes graphs in GML format. GML is a hierarchical key-value
4//! format widely used in network science tools.
5//!
6//! Reference: <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>
7//!
8//! Structural keys (`directed`, `id`, `source`, `target`) drive graph
9//! construction. All other keys inside `node` and `edge` blocks are stored
10//! as vertex/edge attributes via the [`Graph`] attribute system. Graph-level
11//! keys (e.g. `Creator`) are stored as graph attributes.
12
13use std::collections::HashMap;
14use std::io::{BufRead, BufReader, Read, Write};
15
16use crate::core::attributes::AttributeValue;
17use crate::core::{Graph, IgraphError, IgraphResult};
18
19/// Read a graph from GML format.
20///
21/// Parses the first `graph [ ... ]` block. Extracts `directed` (0 or 1,
22/// default 0), `node [ id N ]` entries, and `edge [ source N target N ]`
23/// entries. Non-structural keys within node/edge blocks (e.g. `label`,
24/// `weight`) are stored as vertex/edge attributes.
25///
26/// Node ids need not be contiguous or start at 0 — the reader maps them
27/// to internal vertex indices in the order they appear.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, read_gml, AttributeValue};
33///
34/// let gml = b"graph [
35///   directed 0
36///   node [ id 1 label \"Alice\" ]
37///   node [ id 2 label \"Bob\" ]
38///   edge [ source 1 target 2 weight 1.5 ]
39/// ]";
40/// let g = read_gml(&gml[..]).unwrap();
41/// assert_eq!(g.vcount(), 2);
42/// assert_eq!(g.ecount(), 1);
43/// assert!(!g.is_directed());
44/// assert_eq!(
45///     g.vertex_attribute("label", 0).and_then(|v| v.as_str()),
46///     Some("Alice"),
47/// );
48/// assert_eq!(
49///     g.edge_attribute("weight", 0).and_then(|v| v.as_f64()),
50///     Some(1.5),
51/// );
52/// ```
53pub fn read_gml<R: Read>(input: R) -> IgraphResult<Graph> {
54    let reader = BufReader::new(input);
55    let tokens = tokenize(reader)?;
56    parse_graph(&tokens)
57}
58
59/// Write a graph in GML format, including attributes.
60///
61/// Produces a valid GML file with `graph [ directed 0/1 node [ id N ... ]
62/// ... edge [ source N target N ... ] ... ]`. Graph, vertex, and edge
63/// attributes are written as additional key-value pairs.
64///
65/// # Examples
66///
67/// ```
68/// use rust_igraph::{Graph, write_gml, read_gml, AttributeValue};
69///
70/// let mut g = Graph::new(3, true).unwrap();
71/// g.add_edge(0, 1).unwrap();
72/// g.add_edge(1, 2).unwrap();
73/// g.set_vertex_attribute("label", 0, "A".into()).unwrap();
74/// g.set_edge_attribute("weight", 0, 2.5.into()).unwrap();
75///
76/// let mut buf = Vec::new();
77/// write_gml(&g, &mut buf).unwrap();
78/// let s = String::from_utf8(buf).unwrap();
79/// assert!(s.contains("directed 1"));
80/// assert!(s.contains("label \"A\""));
81/// assert!(s.contains("weight 2.5"));
82///
83/// // Round-trip
84/// let g2 = read_gml(s.as_bytes()).unwrap();
85/// assert_eq!(g2.vcount(), 3);
86/// assert_eq!(g2.ecount(), 2);
87/// assert!(g2.is_directed());
88/// ```
89pub fn write_gml<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
90    writeln!(writer, "graph")?;
91    writeln!(writer, "[")?;
92    writeln!(writer, "  directed {}", i32::from(graph.is_directed()))?;
93
94    write_graph_attrs(graph, writer)?;
95    write_node_blocks(graph, writer)?;
96    write_edge_blocks(graph, writer)?;
97
98    writeln!(writer, "]")?;
99    Ok(())
100}
101
102fn write_graph_attrs<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
103    for &name in &graph.graph_attribute_names() {
104        if let Some(val) = graph.graph_attribute(name) {
105            write!(writer, "  {name} ")?;
106            write_gml_value(val, writer)?;
107            writeln!(writer)?;
108        }
109    }
110    Ok(())
111}
112
113fn write_node_blocks<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
114    let attr_names = graph.vertex_attribute_names();
115    for v in 0..graph.vcount() {
116        writeln!(writer, "  node")?;
117        writeln!(writer, "  [")?;
118        writeln!(writer, "    id {v}")?;
119        for &name in &attr_names {
120            if let Some(val) = graph.vertex_attribute(name, v) {
121                write!(writer, "    {name} ")?;
122                write_gml_value(val, writer)?;
123                writeln!(writer)?;
124            }
125        }
126        writeln!(writer, "  ]")?;
127    }
128    Ok(())
129}
130
131fn write_edge_blocks<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
132    let attr_names = graph.edge_attribute_names();
133    for eid in 0..graph.ecount() {
134        let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
135        let (src, tgt) = graph.edge(eid_u32)?;
136        writeln!(writer, "  edge")?;
137        writeln!(writer, "  [")?;
138        writeln!(writer, "    source {src}")?;
139        writeln!(writer, "    target {tgt}")?;
140        for &name in &attr_names {
141            if let Some(val) = graph.edge_attribute(name, eid_u32) {
142                write!(writer, "    {name} ")?;
143                write_gml_value(val, writer)?;
144                writeln!(writer)?;
145            }
146        }
147        writeln!(writer, "  ]")?;
148    }
149    Ok(())
150}
151
152fn write_gml_value<W: Write>(val: &AttributeValue, writer: &mut W) -> IgraphResult<()> {
153    match val {
154        AttributeValue::Numeric(v) => {
155            write!(writer, "{v}")?;
156        }
157        AttributeValue::Boolean(b) => {
158            write!(writer, "{}", i32::from(*b))?;
159        }
160        AttributeValue::String(s) => {
161            write!(writer, "\"{}\"", gml_escape(s))?;
162        }
163    }
164    Ok(())
165}
166
167fn gml_escape(s: &str) -> String {
168    let mut out = String::with_capacity(s.len());
169    for c in s.chars() {
170        match c {
171            '"' => out.push_str("&quot;"),
172            '\\' => out.push_str("\\\\"),
173            '&' => out.push_str("&amp;"),
174            '<' => out.push_str("&lt;"),
175            '>' => out.push_str("&gt;"),
176            _ => out.push(c),
177        }
178    }
179    out
180}
181
182// --- Tokenizer ---
183
184#[derive(Debug, Clone, PartialEq)]
185enum Token {
186    Key(String),
187    Integer(i64),
188    Float(f64),
189    Str(String),
190    Open,  // [
191    Close, // ]
192}
193
194fn tokenize<R: BufRead>(reader: R) -> IgraphResult<Vec<Token>> {
195    let mut tokens = Vec::new();
196    let mut line_no: usize = 0;
197
198    for line_result in reader.lines() {
199        line_no = line_no.wrapping_add(1);
200        let line = line_result?;
201        let trimmed = line.trim();
202
203        if trimmed.is_empty() {
204            continue;
205        }
206
207        let mut chars = trimmed.chars().peekable();
208        while let Some(&ch) = chars.peek() {
209            match ch {
210                ' ' | '\t' | '\r' => {
211                    chars.next();
212                }
213                '[' => {
214                    tokens.push(Token::Open);
215                    chars.next();
216                }
217                ']' => {
218                    tokens.push(Token::Close);
219                    chars.next();
220                }
221                '"' => {
222                    chars.next();
223                    let s = read_quoted_string(&mut chars, line_no)?;
224                    let decoded = decode_entities(&s);
225                    tokens.push(Token::Str(decoded));
226                }
227                '#' => break,
228                _ => {
229                    let word = read_word(&mut chars);
230                    if word.is_empty() {
231                        return Err(IgraphError::Parse {
232                            line: line_no,
233                            message: format!("unexpected character '{ch}'"),
234                        });
235                    }
236                    if let Ok(i) = word.parse::<i64>() {
237                        tokens.push(Token::Integer(i));
238                    } else if let Ok(f) = parse_gml_float(&word) {
239                        tokens.push(Token::Float(f));
240                    } else {
241                        tokens.push(Token::Key(word));
242                    }
243                }
244            }
245        }
246    }
247
248    Ok(tokens)
249}
250
251fn read_quoted_string(
252    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
253    line_no: usize,
254) -> IgraphResult<String> {
255    let mut s = String::new();
256    let mut escaped = false;
257    loop {
258        match chars.next() {
259            None => {
260                return Err(IgraphError::Parse {
261                    line: line_no,
262                    message: "unterminated string".into(),
263                });
264            }
265            Some('\\') if !escaped => {
266                escaped = true;
267            }
268            Some('"') if !escaped => break,
269            Some(c) => {
270                if escaped {
271                    match c {
272                        'n' => s.push('\n'),
273                        't' => s.push('\t'),
274                        '\\' => s.push('\\'),
275                        '"' => s.push('"'),
276                        _ => {
277                            s.push('\\');
278                            s.push(c);
279                        }
280                    }
281                    escaped = false;
282                } else {
283                    s.push(c);
284                }
285            }
286        }
287    }
288    Ok(s)
289}
290
291fn read_word(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
292    let mut word = String::new();
293    while let Some(&c) = chars.peek() {
294        if c == ' ' || c == '\t' || c == '[' || c == ']' || c == '"' {
295            break;
296        }
297        word.push(c);
298        chars.next();
299    }
300    word
301}
302
303fn parse_gml_float(s: &str) -> Result<f64, ()> {
304    let lower = s.to_ascii_lowercase();
305    match lower.as_str() {
306        "inf" | "+inf" => Ok(f64::INFINITY),
307        "-inf" => Ok(f64::NEG_INFINITY),
308        "nan" => Ok(f64::NAN),
309        _ => s.parse::<f64>().map_err(|_| ()),
310    }
311}
312
313fn decode_entities(s: &str) -> String {
314    s.replace("&amp;", "&")
315        .replace("&quot;", "\"")
316        .replace("&apos;", "'")
317        .replace("&lt;", "<")
318        .replace("&gt;", ">")
319}
320
321// --- Parser ---
322
323struct NodeAttrs {
324    id: i64,
325    attrs: Vec<(String, AttributeValue)>,
326}
327
328struct EdgeAttrs {
329    source: i64,
330    target: i64,
331    attrs: Vec<(String, AttributeValue)>,
332}
333
334fn parse_graph(tokens: &[Token]) -> IgraphResult<Graph> {
335    let mut pos = 0;
336
337    // Collect top-level (graph-level) attributes before "graph" keyword.
338    let mut graph_attrs: Vec<(String, AttributeValue)> = Vec::new();
339
340    while pos < tokens.len() {
341        match &tokens[pos] {
342            Token::Key(k) if k == "graph" => {
343                pos += 1;
344                break;
345            }
346            Token::Key(k) => {
347                let key = k.clone();
348                pos += 1;
349                if let Some((val, new_pos)) = try_read_simple_value(tokens, pos) {
350                    graph_attrs.push((key, val));
351                    pos = new_pos;
352                } else {
353                    pos = skip_value(tokens, pos);
354                }
355            }
356            _ => {
357                pos += 1;
358            }
359        }
360    }
361
362    if pos >= tokens.len() {
363        return Err(IgraphError::Parse {
364            line: 0,
365            message: "no 'graph' block found in GML input".into(),
366        });
367    }
368    if tokens[pos] != Token::Open {
369        return Err(IgraphError::Parse {
370            line: 0,
371            message: "expected '[' after 'graph'".into(),
372        });
373    }
374    pos += 1;
375
376    let mut directed = false;
377    let mut nodes: Vec<NodeAttrs> = Vec::new();
378    let mut edges: Vec<EdgeAttrs> = Vec::new();
379
380    while pos < tokens.len() && tokens[pos] != Token::Close {
381        match &tokens[pos] {
382            Token::Key(k) => {
383                let key = k.clone();
384                pos += 1;
385
386                match key.as_str() {
387                    "directed" => {
388                        if let Some(Token::Integer(v)) = tokens.get(pos) {
389                            directed = *v != 0;
390                            pos += 1;
391                        } else {
392                            pos = skip_value(tokens, pos);
393                        }
394                    }
395                    "node" if pos < tokens.len() && tokens[pos] == Token::Open => {
396                        pos += 1;
397                        let (node, new_pos) = parse_node(tokens, pos)?;
398                        nodes.push(node);
399                        pos = new_pos;
400                    }
401                    "edge" if pos < tokens.len() && tokens[pos] == Token::Open => {
402                        pos += 1;
403                        let (edge, new_pos) = parse_edge(tokens, pos)?;
404                        edges.push(edge);
405                        pos = new_pos;
406                    }
407                    _ => {
408                        if let Some((val, new_pos)) = try_read_simple_value(tokens, pos) {
409                            graph_attrs.push((key, val));
410                            pos = new_pos;
411                        } else {
412                            pos = skip_value(tokens, pos);
413                        }
414                    }
415                }
416            }
417            _ => {
418                return Err(IgraphError::Parse {
419                    line: 0,
420                    message: format!("unexpected token in graph block: {:?}", tokens[pos]),
421                });
422            }
423        }
424    }
425
426    build_graph(directed, &nodes, &edges, &graph_attrs)
427}
428
429fn try_read_simple_value(tokens: &[Token], pos: usize) -> Option<(AttributeValue, usize)> {
430    match tokens.get(pos)? {
431        #[allow(clippy::cast_precision_loss)]
432        Token::Integer(i) => Some((AttributeValue::Numeric(*i as f64), pos + 1)),
433        Token::Float(f) => Some((AttributeValue::Numeric(*f), pos + 1)),
434        Token::Str(s) => Some((AttributeValue::String(s.clone()), pos + 1)),
435        _ => None,
436    }
437}
438
439fn parse_node(tokens: &[Token], mut pos: usize) -> IgraphResult<(NodeAttrs, usize)> {
440    let mut node_id: Option<i64> = None;
441    let mut attrs: Vec<(String, AttributeValue)> = Vec::new();
442
443    while pos < tokens.len() && tokens[pos] != Token::Close {
444        match &tokens[pos] {
445            Token::Key(k) => {
446                let key = k.clone();
447                pos += 1;
448                if key == "id" {
449                    if let Some(Token::Integer(v)) = tokens.get(pos) {
450                        node_id = Some(*v);
451                        pos += 1;
452                    } else {
453                        pos = skip_value(tokens, pos);
454                    }
455                } else if let Some((val, new_pos)) = try_read_simple_value(tokens, pos) {
456                    attrs.push((key, val));
457                    pos = new_pos;
458                } else {
459                    pos = skip_value(tokens, pos);
460                }
461            }
462            _ => {
463                return Err(IgraphError::Parse {
464                    line: 0,
465                    message: format!("unexpected token in node block: {:?}", tokens[pos]),
466                });
467            }
468        }
469    }
470
471    if pos < tokens.len() && tokens[pos] == Token::Close {
472        pos += 1;
473    }
474
475    let id = node_id.ok_or_else(|| IgraphError::Parse {
476        line: 0,
477        message: "node without 'id' field".into(),
478    })?;
479
480    Ok((NodeAttrs { id, attrs }, pos))
481}
482
483fn parse_edge(tokens: &[Token], mut pos: usize) -> IgraphResult<(EdgeAttrs, usize)> {
484    let mut source: Option<i64> = None;
485    let mut target: Option<i64> = None;
486    let mut attrs: Vec<(String, AttributeValue)> = Vec::new();
487
488    while pos < tokens.len() && tokens[pos] != Token::Close {
489        match &tokens[pos] {
490            Token::Key(k) => {
491                let key = k.clone();
492                pos += 1;
493                match key.as_str() {
494                    "source" => {
495                        if let Some(Token::Integer(v)) = tokens.get(pos) {
496                            source = Some(*v);
497                            pos += 1;
498                        } else {
499                            pos = skip_value(tokens, pos);
500                        }
501                    }
502                    "target" => {
503                        if let Some(Token::Integer(v)) = tokens.get(pos) {
504                            target = Some(*v);
505                            pos += 1;
506                        } else {
507                            pos = skip_value(tokens, pos);
508                        }
509                    }
510                    _ => {
511                        if let Some((val, new_pos)) = try_read_simple_value(tokens, pos) {
512                            attrs.push((key, val));
513                            pos = new_pos;
514                        } else {
515                            pos = skip_value(tokens, pos);
516                        }
517                    }
518                }
519            }
520            _ => {
521                return Err(IgraphError::Parse {
522                    line: 0,
523                    message: format!("unexpected token in edge block: {:?}", tokens[pos]),
524                });
525            }
526        }
527    }
528
529    if pos < tokens.len() && tokens[pos] == Token::Close {
530        pos += 1;
531    }
532
533    let src = source.ok_or_else(|| IgraphError::Parse {
534        line: 0,
535        message: "edge without 'source' field".into(),
536    })?;
537    let tgt = target.ok_or_else(|| IgraphError::Parse {
538        line: 0,
539        message: "edge without 'target' field".into(),
540    })?;
541
542    Ok((
543        EdgeAttrs {
544            source: src,
545            target: tgt,
546            attrs,
547        },
548        pos,
549    ))
550}
551
552fn skip_value(tokens: &[Token], mut pos: usize) -> usize {
553    if pos >= tokens.len() {
554        return pos;
555    }
556    match &tokens[pos] {
557        Token::Integer(_) | Token::Float(_) | Token::Str(_) | Token::Key(_) => pos + 1,
558        Token::Open => {
559            pos += 1;
560            let mut depth: u32 = 1;
561            while pos < tokens.len() && depth > 0 {
562                match &tokens[pos] {
563                    Token::Open => depth = depth.saturating_add(1),
564                    Token::Close => depth = depth.saturating_sub(1),
565                    _ => {}
566                }
567                pos += 1;
568            }
569            pos
570        }
571        Token::Close => pos,
572    }
573}
574
575fn build_graph(
576    directed: bool,
577    nodes: &[NodeAttrs],
578    edges: &[EdgeAttrs],
579    graph_attrs: &[(String, AttributeValue)],
580) -> IgraphResult<Graph> {
581    let mut id_map: HashMap<i64, u32> = HashMap::with_capacity(nodes.len());
582    for (idx, node) in nodes.iter().enumerate() {
583        let internal_id =
584            u32::try_from(idx).map_err(|_| IgraphError::Internal("node index overflow"))?;
585        if id_map.insert(node.id, internal_id).is_some() {
586            return Err(IgraphError::Parse {
587                line: 0,
588                message: format!("duplicate node id {}", node.id),
589            });
590        }
591    }
592
593    let n = u32::try_from(nodes.len()).map_err(|_| IgraphError::Internal("node count overflow"))?;
594    let mut graph = Graph::new(n, directed)?;
595
596    // Apply graph-level attributes.
597    for (key, val) in graph_attrs {
598        graph.set_graph_attribute(key, val.clone());
599    }
600
601    // Apply vertex attributes.
602    for (idx, node) in nodes.iter().enumerate() {
603        let vid = u32::try_from(idx).map_err(|_| IgraphError::Internal("vertex id overflow"))?;
604        for (key, val) in &node.attrs {
605            graph.set_vertex_attribute(key, vid, val.clone())?;
606        }
607    }
608
609    // Add edges and apply edge attributes.
610    for (eid_idx, edge) in edges.iter().enumerate() {
611        let src = id_map.get(&edge.source).ok_or_else(|| IgraphError::Parse {
612            line: 0,
613            message: format!("edge references unknown node id {}", edge.source),
614        })?;
615        let tgt = id_map.get(&edge.target).ok_or_else(|| IgraphError::Parse {
616            line: 0,
617            message: format!("edge references unknown node id {}", edge.target),
618        })?;
619        graph.add_edge(*src, *tgt)?;
620
621        let eid = u32::try_from(eid_idx).map_err(|_| IgraphError::Internal("edge id overflow"))?;
622        for (key, val) in &edge.attrs {
623            graph.set_edge_attribute(key, eid, val.clone())?;
624        }
625    }
626
627    Ok(graph)
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    #[test]
635    fn test_empty_graph() {
636        let gml = b"graph [ ]";
637        let g = read_gml(&gml[..]).unwrap();
638        assert_eq!(g.vcount(), 0);
639        assert_eq!(g.ecount(), 0);
640        assert!(!g.is_directed());
641    }
642
643    #[test]
644    fn test_directed_flag() {
645        let gml = b"graph [ directed 1 node [ id 0 ] node [ id 1 ] edge [ source 0 target 1 ] ]";
646        let g = read_gml(&gml[..]).unwrap();
647        assert!(g.is_directed());
648        assert_eq!(g.vcount(), 2);
649        assert_eq!(g.ecount(), 1);
650    }
651
652    #[test]
653    fn test_undirected_default() {
654        let gml = b"graph [ node [ id 0 ] node [ id 1 ] edge [ source 0 target 1 ] ]";
655        let g = read_gml(&gml[..]).unwrap();
656        assert!(!g.is_directed());
657    }
658
659    #[test]
660    fn test_non_contiguous_ids() {
661        let gml = b"graph [\n  node [ id 10 ]\n  node [ id 20 ]\n  node [ id 30 ]\n  edge [ source 10 target 30 ]\n]";
662        let g = read_gml(&gml[..]).unwrap();
663        assert_eq!(g.vcount(), 3);
664        assert_eq!(g.ecount(), 1);
665        let (src, tgt) = g.edge(0).unwrap();
666        assert_eq!(src, 0);
667        assert_eq!(tgt, 2);
668    }
669
670    #[test]
671    fn test_multiline_with_attributes() {
672        let gml = b"Creator \"test\"\ngraph\n[\n  directed 0\n  node\n  [\n    id 0\n    label \"A\"\n  ]\n  node\n  [\n    id 1\n    label \"B\"\n  ]\n  edge\n  [\n    source 0\n    target 1\n    weight 1.5\n  ]\n]\n";
673        let g = read_gml(&gml[..]).unwrap();
674        assert_eq!(g.vcount(), 2);
675        assert_eq!(g.ecount(), 1);
676        assert_eq!(
677            g.vertex_attribute("label", 0).and_then(|v| v.as_str()),
678            Some("A"),
679        );
680        assert_eq!(
681            g.vertex_attribute("label", 1).and_then(|v| v.as_str()),
682            Some("B"),
683        );
684        let w = g
685            .edge_attribute("weight", 0)
686            .and_then(AttributeValue::as_f64);
687        assert!((w.unwrap() - 1.5).abs() < f64::EPSILON);
688        assert_eq!(
689            g.graph_attribute("Creator").and_then(|v| v.as_str()),
690            Some("test"),
691        );
692    }
693
694    #[test]
695    fn test_self_loop() {
696        let gml = b"graph [ node [ id 0 ] edge [ source 0 target 0 ] ]";
697        let g = read_gml(&gml[..]).unwrap();
698        assert_eq!(g.ecount(), 1);
699        let (s, t) = g.edge(0).unwrap();
700        assert_eq!(s, t);
701    }
702
703    #[test]
704    fn test_multiple_edges() {
705        let gml = b"graph [ node [ id 0 ] node [ id 1 ] edge [ source 0 target 1 ] edge [ source 0 target 1 ] ]";
706        let g = read_gml(&gml[..]).unwrap();
707        assert_eq!(g.ecount(), 2);
708    }
709
710    #[test]
711    fn test_error_missing_graph() {
712        let gml = b"node [ id 0 ]";
713        let result = read_gml(&gml[..]);
714        assert!(result.is_err());
715    }
716
717    #[test]
718    fn test_error_duplicate_node_id() {
719        let gml = b"graph [ node [ id 0 ] node [ id 0 ] ]";
720        let result = read_gml(&gml[..]);
721        assert!(result.is_err());
722    }
723
724    #[test]
725    fn test_error_unknown_edge_target() {
726        let gml = b"graph [ node [ id 0 ] edge [ source 0 target 99 ] ]";
727        let result = read_gml(&gml[..]);
728        assert!(result.is_err());
729    }
730
731    #[test]
732    fn test_error_node_without_id() {
733        let gml = b"graph [ node [ label \"x\" ] ]";
734        let result = read_gml(&gml[..]);
735        assert!(result.is_err());
736    }
737
738    #[test]
739    fn test_error_edge_without_source() {
740        let gml = b"graph [ node [ id 0 ] node [ id 1 ] edge [ target 1 ] ]";
741        let result = read_gml(&gml[..]);
742        assert!(result.is_err());
743    }
744
745    #[test]
746    fn test_write_read_round_trip_undirected() {
747        let mut g = Graph::with_vertices(4);
748        g.add_edge(0, 1).unwrap();
749        g.add_edge(1, 2).unwrap();
750        g.add_edge(2, 3).unwrap();
751        g.add_edge(3, 0).unwrap();
752
753        let mut buf = Vec::new();
754        write_gml(&g, &mut buf).unwrap();
755
756        let g2 = read_gml(&buf[..]).unwrap();
757        assert_eq!(g2.vcount(), 4);
758        assert_eq!(g2.ecount(), 4);
759        assert!(!g2.is_directed());
760    }
761
762    #[test]
763    fn test_write_read_round_trip_directed() {
764        let mut g = Graph::new(3, true).unwrap();
765        g.add_edge(0, 1).unwrap();
766        g.add_edge(1, 2).unwrap();
767        g.add_edge(2, 0).unwrap();
768
769        let mut buf = Vec::new();
770        write_gml(&g, &mut buf).unwrap();
771
772        let g2 = read_gml(&buf[..]).unwrap();
773        assert_eq!(g2.vcount(), 3);
774        assert_eq!(g2.ecount(), 3);
775        assert!(g2.is_directed());
776
777        for eid in 0..3u32 {
778            let (s1, t1) = g.edge(eid).unwrap();
779            let (s2, t2) = g2.edge(eid).unwrap();
780            assert_eq!(s1, s2);
781            assert_eq!(t1, t2);
782        }
783    }
784
785    #[test]
786    fn test_write_empty() {
787        let g = Graph::with_vertices(0);
788        let mut buf = Vec::new();
789        write_gml(&g, &mut buf).unwrap();
790        let s = String::from_utf8(buf).unwrap();
791        assert!(s.contains("graph"));
792        assert!(s.contains("directed 0"));
793    }
794
795    #[test]
796    fn test_string_with_entities() {
797        let gml = b"graph [ node [ id 0 label \"a&amp;b\" ] ]";
798        let g = read_gml(&gml[..]).unwrap();
799        assert_eq!(g.vcount(), 1);
800        assert_eq!(
801            g.vertex_attribute("label", 0).and_then(|v| v.as_str()),
802            Some("a&b"),
803        );
804    }
805
806    #[test]
807    fn test_top_level_creator_as_graph_attr() {
808        let gml = b"Creator \"Someone\"\nVersion 1\ngraph [ node [ id 0 ] ]";
809        let g = read_gml(&gml[..]).unwrap();
810        assert_eq!(g.vcount(), 1);
811        assert_eq!(
812            g.graph_attribute("Creator").and_then(|v| v.as_str()),
813            Some("Someone"),
814        );
815    }
816
817    #[test]
818    fn test_negative_node_id() {
819        let gml = b"graph [ node [ id -5 ] node [ id -3 ] edge [ source -5 target -3 ] ]";
820        let g = read_gml(&gml[..]).unwrap();
821        assert_eq!(g.vcount(), 2);
822        assert_eq!(g.ecount(), 1);
823    }
824
825    #[test]
826    fn test_lesmis_style_header() {
827        let gml = b"Creator \"Mark Newman on Fri Jul 21 12:44:53 2006\"\ngraph\n[\n  node\n  [\n    id 0\n    label \"Myriel\"\n  ]\n  node\n  [\n    id 1\n    label \"Napoleon\"\n  ]\n  edge\n  [\n    source 0\n    target 1\n    value 1\n  ]\n]\n";
828        let g = read_gml(&gml[..]).unwrap();
829        assert_eq!(g.vcount(), 2);
830        assert_eq!(g.ecount(), 1);
831        assert_eq!(
832            g.vertex_attribute("label", 0).and_then(|v| v.as_str()),
833            Some("Myriel"),
834        );
835        assert_eq!(
836            g.vertex_attribute("label", 1).and_then(|v| v.as_str()),
837            Some("Napoleon"),
838        );
839        let v = g
840            .edge_attribute("value", 0)
841            .and_then(AttributeValue::as_f64);
842        assert!((v.unwrap() - 1.0).abs() < f64::EPSILON);
843    }
844
845    #[test]
846    fn roundtrip_with_attributes() {
847        let mut g = Graph::from_edges(&[(0, 1), (1, 2)], false, None).unwrap();
848        g.set_vertex_attribute("label", 0, "Alice".into()).unwrap();
849        g.set_vertex_attribute("label", 1, "Bob".into()).unwrap();
850        g.set_vertex_attribute("label", 2, "Carol".into()).unwrap();
851        g.set_edge_attribute("weight", 0, 1.5.into()).unwrap();
852        g.set_edge_attribute("weight", 1, 2.5.into()).unwrap();
853        g.set_graph_attribute("name", "test_graph".into());
854
855        let mut buf = Vec::new();
856        write_gml(&g, &mut buf).unwrap();
857
858        let g2 = read_gml(&buf[..]).unwrap();
859        assert_eq!(g2.vcount(), 3);
860        assert_eq!(g2.ecount(), 2);
861        assert_eq!(
862            g2.vertex_attribute("label", 0).and_then(|v| v.as_str()),
863            Some("Alice"),
864        );
865        assert_eq!(
866            g2.vertex_attribute("label", 2).and_then(|v| v.as_str()),
867            Some("Carol"),
868        );
869        let w = g2
870            .edge_attribute("weight", 0)
871            .and_then(AttributeValue::as_f64);
872        assert!((w.unwrap() - 1.5).abs() < f64::EPSILON);
873        assert_eq!(
874            g2.graph_attribute("name").and_then(|v| v.as_str()),
875            Some("test_graph"),
876        );
877    }
878
879    #[test]
880    fn write_boolean_attribute() {
881        let mut g = Graph::with_vertices(2);
882        g.add_edge(0, 1).unwrap();
883        g.set_vertex_attribute("active", 0, true.into()).unwrap();
884        g.set_vertex_attribute("active", 1, false.into()).unwrap();
885
886        let mut buf = Vec::new();
887        write_gml(&g, &mut buf).unwrap();
888        let s = String::from_utf8(buf).unwrap();
889        assert!(s.contains("active 1"));
890        assert!(s.contains("active 0"));
891    }
892}