scirs2_graph/io/
graphml.rs

1//! GraphML format I/O for graphs
2//!
3//! This module provides functionality for reading and writing graphs in GraphML format.
4//! GraphML is an XML-based format for representing graph structures with rich metadata
5//! and attribute support, widely used in graph analysis tools.
6//!
7//! # Format Specification
8//!
9//! GraphML uses XML structure with the following key elements:
10//! - `<graphml>` - Root element with namespace declarations
11//! - `<key>` - Attribute definitions for nodes/edges
12//! - `<graph>` - Graph container with id and directedness
13//! - `<node>` - Node elements with ids and data attributes
14//! - `<edge>` - Edge elements with source/target and data attributes
15//! - `<data>` - Data elements containing attribute values
16//!
17//! # Examples
18//!
19//! ## Basic GraphML structure:
20//! ```xml
21//! <?xml version="1.0" encoding="UTF-8"?>
22//! <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
23//!          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
24//!          xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
25//!          http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
26//!   <key id="weight" for="edge" attr.name="weight" attr.type="double"/>
27//!   <graph id="G" edgedefault="undirected">
28//!     <node id="1"/>
29//!     <node id="2"/>
30//!     <edge id="e1" source="1" target="2">
31//!       <data key="weight">1.5</data>
32//!     </edge>
33//!   </graph>
34//! </graphml>
35//! ```
36//!
37//! # Usage
38//!
39//! ```rust
40//! use std::fs::File;
41//! use std::io::Write;
42//! use tempfile::NamedTempFile;
43//! use scirs2_graph::base::Graph;
44//! use scirs2_graph::io::graphml::{read_graphml_format, write_graphml_format};
45//!
46//! // Create a temporary file with GraphML data
47//! let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
48//! writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#).expect("Test: failed to write to temp file");
49//! writeln!(temp_file, r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#).expect("Test: failed to write to temp file");
50//! writeln!(temp_file, r#"  <graph id="G" edgedefault="undirected">"#).expect("Test: failed to write to temp file");
51//! writeln!(temp_file, r#"    <node id="1"/>"#).expect("Test: failed to write to temp file");
52//! writeln!(temp_file, r#"    <node id="2"/>"#).expect("Test: failed to write to temp file");
53//! writeln!(temp_file, r#"    <edge source="1" target="2"/>"#).expect("Test: failed to write to temp file");
54//! writeln!(temp_file, r#"  </graph>"#).expect("Test: failed to write to temp file");
55//! writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
56//! temp_file.flush().expect("Test: failed to flush temp file");
57//!
58//! // Read the graph
59//! let graph: Graph<i32, f64> = read_graphml_format(temp_file.path(), false).expect("Test operation failed");
60//! assert_eq!(graph.node_count(), 2);
61//! assert_eq!(graph.edge_count(), 1);
62//! ```
63
64use std::collections::HashMap;
65use std::fs::File;
66use std::io::{BufRead, BufReader, Write};
67use std::path::Path;
68use std::str::FromStr;
69
70use crate::base::{DiGraph, EdgeWeight, Graph, Node};
71use crate::error::{GraphError, Result};
72
73/// Simple XML parser state for GraphML parsing
74#[derive(Debug, Clone, PartialEq, Eq)]
75enum ParseState {
76    /// Looking for graph opening tag
77    SearchingGraph,
78    /// Inside graph, parsing nodes and edges
79    InGraph,
80    /// Finished parsing
81    Done,
82}
83
84/// GraphML key definition for attributes
85#[derive(Debug, Clone)]
86#[allow(dead_code)]
87struct GraphMLKey {
88    /// Key identifier
89    pub id: String,
90    /// Target type: "node", "edge", "graph", "all"
91    pub for_target: String,
92    /// Attribute name
93    pub attr_name: String,
94    /// Attribute type: "boolean", "int", "long", "float", "double", "string"
95    pub attr_type: String,
96    /// Default value if any
97    pub default_value: Option<String>,
98}
99
100/// Read an undirected graph from GraphML format
101///
102/// # Arguments
103///
104/// * `path` - Path to the input file
105/// * `weighted` - Whether to parse edge weights from GraphML data elements
106///
107/// # Returns
108///
109/// * `Ok(Graph)` - The graph read from the file
110/// * `Err(GraphError)` - If there was an error reading or parsing the file
111///
112/// # Format
113///
114/// The GraphML format supports:
115/// - XML-based structure with proper namespaces
116/// - Key definitions for node and edge attributes
117/// - Rich metadata and data elements
118/// - Both directed and undirected graphs
119/// - Hierarchical graph structures (though currently simplified)
120#[allow(dead_code)]
121pub fn read_graphml_format<N, E, P>(path: P, weighted: bool) -> Result<Graph<N, E>>
122where
123    N: Node + std::fmt::Debug + FromStr + Clone,
124    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
125    P: AsRef<Path>,
126{
127    let file = File::open(path)?;
128    let reader = BufReader::new(file);
129    let mut graph = Graph::new();
130    let mut state = ParseState::SearchingGraph;
131    let mut keys = HashMap::new();
132    let mut is_directed = false;
133
134    for (line_num, line_result) in reader.lines().enumerate() {
135        let line = line_result?;
136        let line = line.trim();
137
138        if line.is_empty() || line.starts_with("<?xml") || line.starts_with("<!--") {
139            continue;
140        }
141
142        match state {
143            ParseState::SearchingGraph => {
144                if line.starts_with("<key ") {
145                    if let Some(key) = parse_key_definition(line)? {
146                        keys.insert(key.id.clone(), key);
147                    }
148                } else if line.starts_with("<graph ") {
149                    is_directed = line.contains("edgedefault=\"directed\"");
150                    state = ParseState::InGraph;
151                }
152            }
153            ParseState::InGraph => {
154                if line.starts_with("</graph>") {
155                    state = ParseState::Done;
156                    break;
157                } else if line.starts_with("<node ") {
158                    parse_node_element(line, &mut graph, line_num + 1)?;
159                } else if line.starts_with("<edge ") {
160                    parse_edge_element(line, &mut graph, &keys, weighted, line_num + 1)?;
161                }
162            }
163            ParseState::Done => break,
164        }
165    }
166
167    // Verify we found a graph
168    if state == ParseState::SearchingGraph {
169        return Err(GraphError::Other(
170            "No valid GraphML graph element found".to_string(),
171        ));
172    }
173
174    // Check if this was actually a directed graph but we're reading as undirected
175    if is_directed {
176        return Err(GraphError::Other(
177            "GraphML file contains a directed graph, but undirected graph was requested"
178                .to_string(),
179        ));
180    }
181
182    Ok(graph)
183}
184
185/// Read a directed graph from GraphML format
186///
187/// # Arguments
188///
189/// * `path` - Path to the input file
190/// * `weighted` - Whether to parse edge weights from GraphML data elements
191///
192/// # Returns
193///
194/// * `Ok(DiGraph)` - The directed graph read from the file
195/// * `Err(GraphError)` - If there was an error reading or parsing the file
196#[allow(dead_code)]
197pub fn read_graphml_format_digraph<N, E, P>(path: P, weighted: bool) -> Result<DiGraph<N, E>>
198where
199    N: Node + std::fmt::Debug + FromStr + Clone,
200    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
201    P: AsRef<Path>,
202{
203    let file = File::open(path)?;
204    let reader = BufReader::new(file);
205    let mut graph = DiGraph::new();
206    let mut state = ParseState::SearchingGraph;
207    let mut keys = HashMap::new();
208
209    for (line_num, line_result) in reader.lines().enumerate() {
210        let line = line_result?;
211        let line = line.trim();
212
213        if line.is_empty() || line.starts_with("<?xml") || line.starts_with("<!--") {
214            continue;
215        }
216
217        match state {
218            ParseState::SearchingGraph => {
219                if line.starts_with("<key ") {
220                    if let Some(key) = parse_key_definition(line)? {
221                        keys.insert(key.id.clone(), key);
222                    }
223                } else if line.starts_with("<graph ") {
224                    state = ParseState::InGraph;
225                }
226            }
227            ParseState::InGraph => {
228                if line.starts_with("</graph>") {
229                    state = ParseState::Done;
230                    break;
231                } else if line.starts_with("<node ") {
232                    parse_digraph_node_element(line, &mut graph, line_num + 1)?;
233                } else if line.starts_with("<edge ") {
234                    parse_digraph_edge_element(line, &mut graph, &keys, weighted, line_num + 1)?;
235                }
236            }
237            ParseState::Done => break,
238        }
239    }
240
241    // Verify we found a graph
242    if state == ParseState::SearchingGraph {
243        return Err(GraphError::Other(
244            "No valid GraphML graph element found".to_string(),
245        ));
246    }
247
248    Ok(graph)
249}
250
251/// Write an undirected graph to GraphML format
252///
253/// # Arguments
254///
255/// * `graph` - The graph to write
256/// * `path` - Path to the output file
257/// * `weighted` - Whether to include edge weights as data elements
258///
259/// # Returns
260///
261/// * `Ok(())` - If the graph was written successfully
262/// * `Err(GraphError)` - If there was an error writing the file
263#[allow(dead_code)]
264pub fn write_graphml_format<N, E, Ix, P>(
265    graph: &Graph<N, E, Ix>,
266    path: P,
267    weighted: bool,
268) -> Result<()>
269where
270    N: Node + std::fmt::Debug + std::fmt::Display + Clone,
271    E: EdgeWeight
272        + std::marker::Copy
273        + std::fmt::Debug
274        + std::default::Default
275        + std::fmt::Display
276        + Clone,
277    Ix: petgraph::graph::IndexType,
278    P: AsRef<Path>,
279{
280    let mut file = File::create(path)?;
281
282    // Write XML declaration and GraphML header
283    writeln!(file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
284    writeln!(
285        file,
286        r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
287    )?;
288    writeln!(
289        file,
290        r#"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
291    )?;
292    writeln!(
293        file,
294        r#"         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns"#
295    )?;
296    writeln!(
297        file,
298        r#"         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
299    )?;
300
301    // Write key definitions if weighted
302    if weighted {
303        writeln!(
304            file,
305            r#"  <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
306        )?;
307    }
308
309    // Write graph opening tag
310    writeln!(file, r#"  <graph id="G" edgedefault="undirected">"#)?;
311    writeln!(file, "    <!-- Generated by scirs2-graph -->")?;
312
313    // Write nodes
314    for node in graph.nodes() {
315        writeln!(file, r#"    <node id="{node}"/>"#)?;
316    }
317
318    // Write edges
319    let edges = graph.edges();
320    for (edge_id, edge) in edges.iter().enumerate() {
321        if weighted {
322            writeln!(
323                file,
324                r#"    <edge id="e{}" source="{}" target="{}">"#,
325                edge_id, edge.source, edge.target
326            )?;
327            writeln!(file, r#"      <data key="weight">{}</data>"#, edge.weight)?;
328            writeln!(file, "    </edge>")?;
329        } else {
330            writeln!(
331                file,
332                r#"    <edge id="e{}" source="{}" target="{}"/>"#,
333                edge_id, edge.source, edge.target
334            )?;
335        }
336    }
337
338    // Close graph and graphml
339    writeln!(file, "  </graph>")?;
340    writeln!(file, "</graphml>")?;
341
342    Ok(())
343}
344
345/// Write a directed graph to GraphML format
346///
347/// # Arguments
348///
349/// * `graph` - The directed graph to write
350/// * `path` - Path to the output file
351/// * `weighted` - Whether to include edge weights as data elements
352///
353/// # Returns
354///
355/// * `Ok(())` - If the graph was written successfully
356/// * `Err(GraphError)` - If there was an error writing the file
357#[allow(dead_code)]
358pub fn write_graphml_format_digraph<N, E, Ix, P>(
359    graph: &DiGraph<N, E, Ix>,
360    path: P,
361    weighted: bool,
362) -> Result<()>
363where
364    N: Node + std::fmt::Debug + std::fmt::Display + Clone,
365    E: EdgeWeight
366        + std::marker::Copy
367        + std::fmt::Debug
368        + std::default::Default
369        + std::fmt::Display
370        + Clone,
371    Ix: petgraph::graph::IndexType,
372    P: AsRef<Path>,
373{
374    let mut file = File::create(path)?;
375
376    // Write XML declaration and GraphML header
377    writeln!(file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
378    writeln!(
379        file,
380        r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
381    )?;
382    writeln!(
383        file,
384        r#"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
385    )?;
386    writeln!(
387        file,
388        r#"         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns"#
389    )?;
390    writeln!(
391        file,
392        r#"         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
393    )?;
394
395    // Write key definitions if weighted
396    if weighted {
397        writeln!(
398            file,
399            r#"  <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
400        )?;
401    }
402
403    // Write graph opening tag (directed)
404    writeln!(file, r#"  <graph id="G" edgedefault="directed">"#)?;
405    writeln!(file, "    <!-- Generated by scirs2-graph (directed) -->")?;
406
407    // Write nodes
408    for node in graph.nodes() {
409        writeln!(file, r#"    <node id="{node}"/>"#)?;
410    }
411
412    // Write edges
413    let edges = graph.edges();
414    for (edge_id, edge) in edges.iter().enumerate() {
415        if weighted {
416            writeln!(
417                file,
418                r#"    <edge id="e{}" source="{}" target="{}">"#,
419                edge_id, edge.source, edge.target
420            )?;
421            writeln!(file, r#"      <data key="weight">{}</data>"#, edge.weight)?;
422            writeln!(file, "    </edge>")?;
423        } else {
424            writeln!(
425                file,
426                r#"    <edge id="e{}" source="{}" target="{}"/>"#,
427                edge_id, edge.source, edge.target
428            )?;
429        }
430    }
431
432    // Close graph and graphml
433    writeln!(file, "  </graph>")?;
434    writeln!(file, "</graphml>")?;
435
436    Ok(())
437}
438
439// Helper functions
440
441/// Parse a GraphML key definition from XML
442#[allow(dead_code)]
443fn parse_key_definition(line: &str) -> Result<Option<GraphMLKey>> {
444    // Simple attribute parsing for key elements
445    let mut id = None;
446    let mut for_target = None;
447    let mut attr_name = None;
448    let mut attr_type = None;
449
450    // Extract attributes using simple string matching
451    if let Some(id_start) = line.find(r#"id=""#) {
452        let start = id_start + 4;
453        if let Some(end) = line[start..].find('"') {
454            id = Some(line[start..start + end].to_string());
455        }
456    }
457
458    if let Some(for_start) = line.find(r#"for=""#) {
459        let start = for_start + 5;
460        if let Some(end) = line[start..].find('"') {
461            for_target = Some(line[start..start + end].to_string());
462        }
463    }
464
465    if let Some(name_start) = line.find(r#"attr.name=""#) {
466        let start = name_start + 11;
467        if let Some(end) = line[start..].find('"') {
468            attr_name = Some(line[start..start + end].to_string());
469        }
470    }
471
472    if let Some(type_start) = line.find(r#"attr.type=""#) {
473        let start = type_start + 11;
474        if let Some(end) = line[start..].find('"') {
475            attr_type = Some(line[start..start + end].to_string());
476        }
477    }
478
479    if let (Some(id), Some(for_target), Some(attr_name), Some(attr_type)) =
480        (id, for_target, attr_name, attr_type)
481    {
482        Ok(Some(GraphMLKey {
483            id,
484            for_target,
485            attr_name,
486            attr_type,
487            default_value: None,
488        }))
489    } else {
490        Ok(None)
491    }
492}
493
494/// Parse a GraphML node element for undirected graphs
495#[allow(dead_code)]
496fn parse_node_element<N, E>(line: &str, graph: &mut Graph<N, E>, linenum: usize) -> Result<()>
497where
498    N: Node + std::fmt::Debug + FromStr + Clone,
499    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
500{
501    // Extract node id
502    if let Some(id_start) = line.find(r#"id=""#) {
503        let start = id_start + 4;
504        if let Some(end) = line[start..].find('"') {
505            let node_id = &line[start..start + end];
506            let _node = N::from_str(node_id).map_err(|_| {
507                GraphError::Other(format!(
508                    "Failed to parse node ID '{node_id}' on line {linenum}"
509                ))
510            })?;
511            // Nodes will be added automatically when edges are added
512        }
513    }
514
515    Ok(())
516}
517
518/// Parse a GraphML node element for directed graphs
519#[allow(dead_code)]
520fn parse_digraph_node_element<N, E>(
521    line: &str,
522    graph: &mut DiGraph<N, E>,
523    line_num: usize,
524) -> Result<()>
525where
526    N: Node + std::fmt::Debug + FromStr + Clone,
527    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
528{
529    // Extract node id
530    if let Some(id_start) = line.find(r#"id=""#) {
531        let start = id_start + 4;
532        if let Some(end) = line[start..].find('"') {
533            let node_id = &line[start..start + end];
534            let _node = N::from_str(node_id).map_err(|_| {
535                GraphError::Other(format!(
536                    "Failed to parse node ID '{node_id}' on line {line_num}"
537                ))
538            })?;
539            // Nodes will be added automatically when edges are added
540        }
541    }
542
543    Ok(())
544}
545
546/// Parse a GraphML edge element for undirected graphs
547#[allow(dead_code)]
548fn parse_edge_element<N, E>(
549    line: &str,
550    graph: &mut Graph<N, E>,
551    _keys: &HashMap<String, GraphMLKey>,
552    _weighted: bool,
553    line_num: usize,
554) -> Result<()>
555where
556    N: Node + std::fmt::Debug + FromStr + Clone,
557    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
558{
559    let mut source_id = None;
560    let mut target_id = None;
561
562    // Extract source
563    if let Some(source_start) = line.find(r#"source=""#) {
564        let start = source_start + 8;
565        if let Some(end) = line[start..].find('"') {
566            source_id = Some(&line[start..start + end]);
567        }
568    }
569
570    // Extract target
571    if let Some(target_start) = line.find(r#"target=""#) {
572        let start = target_start + 8;
573        if let Some(end) = line[start..].find('"') {
574            target_id = Some(&line[start..start + end]);
575        }
576    }
577
578    if let (Some(source_id), Some(target_id)) = (source_id, target_id) {
579        let source_node = N::from_str(source_id).map_err(|_| {
580            GraphError::Other(format!(
581                "Failed to parse source node '{source_id}' on line {line_num}"
582            ))
583        })?;
584
585        let target_node = N::from_str(target_id).map_err(|_| {
586            GraphError::Other(format!(
587                "Failed to parse target node '{target_id}' on line {line_num}"
588            ))
589        })?;
590
591        // For now, use default weight (proper data parsing would be more complex)
592        let weight = E::default();
593
594        graph.add_edge(source_node, target_node, weight)?;
595    } else {
596        return Err(GraphError::Other(format!(
597            "Invalid edge element - missing source or target on line {line_num}"
598        )));
599    }
600
601    Ok(())
602}
603
604/// Parse a GraphML edge element for directed graphs
605#[allow(dead_code)]
606fn parse_digraph_edge_element<N, E>(
607    line: &str,
608    graph: &mut DiGraph<N, E>,
609    _keys: &HashMap<String, GraphMLKey>,
610    _weighted: bool,
611    line_num: usize,
612) -> Result<()>
613where
614    N: Node + std::fmt::Debug + FromStr + Clone,
615    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
616{
617    let mut source_id = None;
618    let mut target_id = None;
619
620    // Extract source
621    if let Some(source_start) = line.find(r#"source=""#) {
622        let start = source_start + 8;
623        if let Some(end) = line[start..].find('"') {
624            source_id = Some(&line[start..start + end]);
625        }
626    }
627
628    // Extract target
629    if let Some(target_start) = line.find(r#"target=""#) {
630        let start = target_start + 8;
631        if let Some(end) = line[start..].find('"') {
632            target_id = Some(&line[start..start + end]);
633        }
634    }
635
636    if let (Some(source_id), Some(target_id)) = (source_id, target_id) {
637        let source_node = N::from_str(source_id).map_err(|_| {
638            GraphError::Other(format!(
639                "Failed to parse source node '{source_id}' on line {line_num}"
640            ))
641        })?;
642
643        let target_node = N::from_str(target_id).map_err(|_| {
644            GraphError::Other(format!(
645                "Failed to parse target node '{target_id}' on line {line_num}"
646            ))
647        })?;
648
649        // For now, use default weight (proper data parsing would be more complex)
650        let weight = E::default();
651
652        graph.add_edge(source_node, target_node, weight)?;
653    } else {
654        return Err(GraphError::Other(format!(
655            "Invalid edge element - missing source or target on line {line_num}"
656        )));
657    }
658
659    Ok(())
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use std::io::Write;
666    use tempfile::NamedTempFile;
667
668    #[test]
669    fn test_read_simple_graphml() {
670        let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
671        writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
672            .expect("Test: failed to write to temp file");
673        writeln!(
674            temp_file,
675            r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
676        )
677        .expect("Test: failed to write to temp file");
678        writeln!(temp_file, r#"  <graph id="G" edgedefault="undirected">"#)
679            .expect("Test: failed to write to temp file");
680        writeln!(temp_file, r#"    <node id="1"/>"#).expect("Test: failed to write to temp file");
681        writeln!(temp_file, r#"    <node id="2"/>"#).expect("Test: failed to write to temp file");
682        writeln!(temp_file, r#"    <node id="3"/>"#).expect("Test: failed to write to temp file");
683        writeln!(temp_file, r#"    <edge source="1" target="2"/>"#)
684            .expect("Test: failed to write to temp file");
685        writeln!(temp_file, r#"    <edge source="2" target="3"/>"#)
686            .expect("Test: failed to write to temp file");
687        writeln!(temp_file, r#"  </graph>"#).expect("Test: failed to write to temp file");
688        writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
689        temp_file.flush().expect("Test: failed to flush temp file");
690
691        let graph: Graph<i32, f64> =
692            read_graphml_format(temp_file.path(), false).expect("Test operation failed");
693
694        assert_eq!(graph.node_count(), 3);
695        assert_eq!(graph.edge_count(), 2);
696    }
697
698    #[test]
699    fn test_read_directed_graphml() {
700        let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
701        writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
702            .expect("Test: failed to write to temp file");
703        writeln!(
704            temp_file,
705            r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
706        )
707        .expect("Test: failed to write to temp file");
708        writeln!(temp_file, r#"  <graph id="G" edgedefault="directed">"#)
709            .expect("Test: failed to write to temp file");
710        writeln!(temp_file, r#"    <node id="1"/>"#).expect("Test: failed to write to temp file");
711        writeln!(temp_file, r#"    <node id="2"/>"#).expect("Test: failed to write to temp file");
712        writeln!(temp_file, r#"    <node id="3"/>"#).expect("Test: failed to write to temp file");
713        writeln!(temp_file, r#"    <edge source="1" target="2"/>"#)
714            .expect("Test: failed to write to temp file");
715        writeln!(temp_file, r#"    <edge source="2" target="3"/>"#)
716            .expect("Test: failed to write to temp file");
717        writeln!(temp_file, r#"  </graph>"#).expect("Test: failed to write to temp file");
718        writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
719        temp_file.flush().expect("Test: failed to flush temp file");
720
721        let graph: DiGraph<i32, f64> =
722            read_graphml_format_digraph(temp_file.path(), false).expect("Test operation failed");
723
724        assert_eq!(graph.node_count(), 3);
725        assert_eq!(graph.edge_count(), 2);
726    }
727
728    #[test]
729    fn test_write_read_roundtrip() {
730        let mut original_graph: Graph<i32, f64> = Graph::new();
731        original_graph
732            .add_edge(1i32, 2i32, 1.5f64)
733            .expect("Test: failed to add edge");
734        original_graph
735            .add_edge(2i32, 3i32, 2.0f64)
736            .expect("Test: failed to add edge");
737
738        let temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
739        write_graphml_format(&original_graph, temp_file.path(), false)
740            .expect("Test operation failed");
741
742        let read_graph: Graph<i32, f64> =
743            read_graphml_format(temp_file.path(), false).expect("Test operation failed");
744
745        assert_eq!(read_graph.node_count(), original_graph.node_count());
746        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
747    }
748
749    #[test]
750    fn test_digraph_write_read_roundtrip() {
751        let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
752        original_graph
753            .add_edge(1i32, 2i32, 1.5f64)
754            .expect("Test: failed to add edge");
755        original_graph
756            .add_edge(2i32, 3i32, 2.0f64)
757            .expect("Test: failed to add edge");
758
759        let temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
760        write_graphml_format_digraph(&original_graph, temp_file.path(), false)
761            .expect("Test operation failed");
762
763        let read_graph: DiGraph<i32, f64> =
764            read_graphml_format_digraph(temp_file.path(), false).expect("Test operation failed");
765
766        assert_eq!(read_graph.node_count(), original_graph.node_count());
767        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
768    }
769
770    #[test]
771    fn test_invalid_xml() {
772        let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
773        writeln!(temp_file, "<invalid>xml</invalid>").expect("Test: failed to write to temp file");
774        temp_file.flush().expect("Test: failed to flush temp file");
775
776        let result: Result<Graph<i32, f64>> = read_graphml_format(temp_file.path(), false);
777        assert!(result.is_err());
778    }
779
780    #[test]
781    fn test_directed_mismatch() {
782        let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
783        writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
784            .expect("Test: failed to write to temp file");
785        writeln!(
786            temp_file,
787            r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
788        )
789        .expect("Test: failed to write to temp file");
790        writeln!(temp_file, r#"  <graph id="G" edgedefault="directed">"#)
791            .expect("Test: failed to write to temp file");
792        writeln!(temp_file, r#"    <node id="1"/>"#).expect("Test: failed to write to temp file");
793        writeln!(temp_file, r#"    <node id="2"/>"#).expect("Test: failed to write to temp file");
794        writeln!(temp_file, r#"    <edge source="1" target="2"/>"#)
795            .expect("Test: failed to write to temp file");
796        writeln!(temp_file, r#"  </graph>"#).expect("Test: failed to write to temp file");
797        writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
798        temp_file.flush().expect("Test: failed to flush temp file");
799
800        // Try to read as undirected graph - should fail
801        let result: Result<Graph<i32, f64>> = read_graphml_format(temp_file.path(), false);
802        assert!(result.is_err());
803    }
804}