1use 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#[derive(Debug, Clone, PartialEq, Eq)]
75enum ParseState {
76 SearchingGraph,
78 InGraph,
80 Done,
82}
83
84#[derive(Debug, Clone)]
86#[allow(dead_code)]
87struct GraphMLKey {
88 pub id: String,
90 pub for_target: String,
92 pub attr_name: String,
94 pub attr_type: String,
96 pub default_value: Option<String>,
98}
99
100#[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 if state == ParseState::SearchingGraph {
169 return Err(GraphError::Other(
170 "No valid GraphML graph element found".to_string(),
171 ));
172 }
173
174 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#[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 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#[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 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 if weighted {
303 writeln!(
304 file,
305 r#" <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
306 )?;
307 }
308
309 writeln!(file, r#" <graph id="G" edgedefault="undirected">"#)?;
311 writeln!(file, " <!-- Generated by scirs2-graph -->")?;
312
313 for node in graph.nodes() {
315 writeln!(file, r#" <node id="{node}"/>"#)?;
316 }
317
318 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 writeln!(file, " </graph>")?;
340 writeln!(file, "</graphml>")?;
341
342 Ok(())
343}
344
345#[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 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 if weighted {
397 writeln!(
398 file,
399 r#" <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
400 )?;
401 }
402
403 writeln!(file, r#" <graph id="G" edgedefault="directed">"#)?;
405 writeln!(file, " <!-- Generated by scirs2-graph (directed) -->")?;
406
407 for node in graph.nodes() {
409 writeln!(file, r#" <node id="{node}"/>"#)?;
410 }
411
412 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 writeln!(file, " </graph>")?;
434 writeln!(file, "</graphml>")?;
435
436 Ok(())
437}
438
439#[allow(dead_code)]
443fn parse_key_definition(line: &str) -> Result<Option<GraphMLKey>> {
444 let mut id = None;
446 let mut for_target = None;
447 let mut attr_name = None;
448 let mut attr_type = None;
449
450 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#[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 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 }
513 }
514
515 Ok(())
516}
517
518#[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 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 }
541 }
542
543 Ok(())
544}
545
546#[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 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 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 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#[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 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 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 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 let result: Result<Graph<i32, f64>> = read_graphml_format(temp_file.path(), false);
802 assert!(result.is_err());
803 }
804}