use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::str::FromStr;
use crate::base::{DiGraph, EdgeWeight, Graph, Node};
use crate::error::{GraphError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParseState {
SearchingGraph,
InGraph,
Done,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct GraphMLKey {
pub id: String,
pub for_target: String,
pub attr_name: String,
pub attr_type: String,
pub default_value: Option<String>,
}
#[allow(dead_code)]
pub fn read_graphml_format<N, E, P>(path: P, weighted: bool) -> Result<Graph<N, E>>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut graph = Graph::new();
let mut state = ParseState::SearchingGraph;
let mut keys = HashMap::new();
let mut is_directed = false;
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result?;
let line = line.trim();
if line.is_empty() || line.starts_with("<?xml") || line.starts_with("<!--") {
continue;
}
match state {
ParseState::SearchingGraph => {
if line.starts_with("<key ") {
if let Some(key) = parse_key_definition(line)? {
keys.insert(key.id.clone(), key);
}
} else if line.starts_with("<graph ") {
is_directed = line.contains("edgedefault=\"directed\"");
state = ParseState::InGraph;
}
}
ParseState::InGraph => {
if line.starts_with("</graph>") {
state = ParseState::Done;
break;
} else if line.starts_with("<node ") {
parse_node_element(line, &mut graph, line_num + 1)?;
} else if line.starts_with("<edge ") {
parse_edge_element(line, &mut graph, &keys, weighted, line_num + 1)?;
}
}
ParseState::Done => break,
}
}
if state == ParseState::SearchingGraph {
return Err(GraphError::Other(
"No valid GraphML graph element found".to_string(),
));
}
if is_directed {
return Err(GraphError::Other(
"GraphML file contains a directed graph, but undirected graph was requested"
.to_string(),
));
}
Ok(graph)
}
#[allow(dead_code)]
pub fn read_graphml_format_digraph<N, E, P>(path: P, weighted: bool) -> Result<DiGraph<N, E>>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut graph = DiGraph::new();
let mut state = ParseState::SearchingGraph;
let mut keys = HashMap::new();
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result?;
let line = line.trim();
if line.is_empty() || line.starts_with("<?xml") || line.starts_with("<!--") {
continue;
}
match state {
ParseState::SearchingGraph => {
if line.starts_with("<key ") {
if let Some(key) = parse_key_definition(line)? {
keys.insert(key.id.clone(), key);
}
} else if line.starts_with("<graph ") {
state = ParseState::InGraph;
}
}
ParseState::InGraph => {
if line.starts_with("</graph>") {
state = ParseState::Done;
break;
} else if line.starts_with("<node ") {
parse_digraph_node_element(line, &mut graph, line_num + 1)?;
} else if line.starts_with("<edge ") {
parse_digraph_edge_element(line, &mut graph, &keys, weighted, line_num + 1)?;
}
}
ParseState::Done => break,
}
}
if state == ParseState::SearchingGraph {
return Err(GraphError::Other(
"No valid GraphML graph element found".to_string(),
));
}
Ok(graph)
}
#[allow(dead_code)]
pub fn write_graphml_format<N, E, Ix, P>(
graph: &Graph<N, E, Ix>,
path: P,
weighted: bool,
) -> Result<()>
where
N: Node + std::fmt::Debug + std::fmt::Display + Clone,
E: EdgeWeight
+ std::marker::Copy
+ std::fmt::Debug
+ std::default::Default
+ std::fmt::Display
+ Clone,
Ix: petgraph::graph::IndexType,
P: AsRef<Path>,
{
let mut file = File::create(path)?;
writeln!(file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
file,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
)?;
writeln!(
file,
r#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
)?;
writeln!(
file,
r#" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns"#
)?;
writeln!(
file,
r#" http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
)?;
if weighted {
writeln!(
file,
r#" <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
)?;
}
writeln!(file, r#" <graph id="G" edgedefault="undirected">"#)?;
writeln!(file, " <!-- Generated by scirs2-graph -->")?;
for node in graph.nodes() {
writeln!(file, r#" <node id="{node}"/>"#)?;
}
let edges = graph.edges();
for (edge_id, edge) in edges.iter().enumerate() {
if weighted {
writeln!(
file,
r#" <edge id="e{}" source="{}" target="{}">"#,
edge_id, edge.source, edge.target
)?;
writeln!(file, r#" <data key="weight">{}</data>"#, edge.weight)?;
writeln!(file, " </edge>")?;
} else {
writeln!(
file,
r#" <edge id="e{}" source="{}" target="{}"/>"#,
edge_id, edge.source, edge.target
)?;
}
}
writeln!(file, " </graph>")?;
writeln!(file, "</graphml>")?;
Ok(())
}
#[allow(dead_code)]
pub fn write_graphml_format_digraph<N, E, Ix, P>(
graph: &DiGraph<N, E, Ix>,
path: P,
weighted: bool,
) -> Result<()>
where
N: Node + std::fmt::Debug + std::fmt::Display + Clone,
E: EdgeWeight
+ std::marker::Copy
+ std::fmt::Debug
+ std::default::Default
+ std::fmt::Display
+ Clone,
Ix: petgraph::graph::IndexType,
P: AsRef<Path>,
{
let mut file = File::create(path)?;
writeln!(file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
file,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns""#
)?;
writeln!(
file,
r#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
)?;
writeln!(
file,
r#" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns"#
)?;
writeln!(
file,
r#" http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">"#
)?;
if weighted {
writeln!(
file,
r#" <key id="weight" for="edge" attr.name="weight" attr.type="double"/>"#
)?;
}
writeln!(file, r#" <graph id="G" edgedefault="directed">"#)?;
writeln!(file, " <!-- Generated by scirs2-graph (directed) -->")?;
for node in graph.nodes() {
writeln!(file, r#" <node id="{node}"/>"#)?;
}
let edges = graph.edges();
for (edge_id, edge) in edges.iter().enumerate() {
if weighted {
writeln!(
file,
r#" <edge id="e{}" source="{}" target="{}">"#,
edge_id, edge.source, edge.target
)?;
writeln!(file, r#" <data key="weight">{}</data>"#, edge.weight)?;
writeln!(file, " </edge>")?;
} else {
writeln!(
file,
r#" <edge id="e{}" source="{}" target="{}"/>"#,
edge_id, edge.source, edge.target
)?;
}
}
writeln!(file, " </graph>")?;
writeln!(file, "</graphml>")?;
Ok(())
}
#[allow(dead_code)]
fn parse_key_definition(line: &str) -> Result<Option<GraphMLKey>> {
let mut id = None;
let mut for_target = None;
let mut attr_name = None;
let mut attr_type = None;
if let Some(id_start) = line.find(r#"id=""#) {
let start = id_start + 4;
if let Some(end) = line[start..].find('"') {
id = Some(line[start..start + end].to_string());
}
}
if let Some(for_start) = line.find(r#"for=""#) {
let start = for_start + 5;
if let Some(end) = line[start..].find('"') {
for_target = Some(line[start..start + end].to_string());
}
}
if let Some(name_start) = line.find(r#"attr.name=""#) {
let start = name_start + 11;
if let Some(end) = line[start..].find('"') {
attr_name = Some(line[start..start + end].to_string());
}
}
if let Some(type_start) = line.find(r#"attr.type=""#) {
let start = type_start + 11;
if let Some(end) = line[start..].find('"') {
attr_type = Some(line[start..start + end].to_string());
}
}
if let (Some(id), Some(for_target), Some(attr_name), Some(attr_type)) =
(id, for_target, attr_name, attr_type)
{
Ok(Some(GraphMLKey {
id,
for_target,
attr_name,
attr_type,
default_value: None,
}))
} else {
Ok(None)
}
}
#[allow(dead_code)]
fn parse_node_element<N, E>(line: &str, graph: &mut Graph<N, E>, linenum: usize) -> Result<()>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
{
if let Some(id_start) = line.find(r#"id=""#) {
let start = id_start + 4;
if let Some(end) = line[start..].find('"') {
let node_id = &line[start..start + end];
let _node = N::from_str(node_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse node ID '{node_id}' on line {linenum}"
))
})?;
}
}
Ok(())
}
#[allow(dead_code)]
fn parse_digraph_node_element<N, E>(
line: &str,
graph: &mut DiGraph<N, E>,
line_num: usize,
) -> Result<()>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
{
if let Some(id_start) = line.find(r#"id=""#) {
let start = id_start + 4;
if let Some(end) = line[start..].find('"') {
let node_id = &line[start..start + end];
let _node = N::from_str(node_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse node ID '{node_id}' on line {line_num}"
))
})?;
}
}
Ok(())
}
#[allow(dead_code)]
fn parse_edge_element<N, E>(
line: &str,
graph: &mut Graph<N, E>,
_keys: &HashMap<String, GraphMLKey>,
_weighted: bool,
line_num: usize,
) -> Result<()>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
{
let mut source_id = None;
let mut target_id = None;
if let Some(source_start) = line.find(r#"source=""#) {
let start = source_start + 8;
if let Some(end) = line[start..].find('"') {
source_id = Some(&line[start..start + end]);
}
}
if let Some(target_start) = line.find(r#"target=""#) {
let start = target_start + 8;
if let Some(end) = line[start..].find('"') {
target_id = Some(&line[start..start + end]);
}
}
if let (Some(source_id), Some(target_id)) = (source_id, target_id) {
let source_node = N::from_str(source_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse source node '{source_id}' on line {line_num}"
))
})?;
let target_node = N::from_str(target_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse target node '{target_id}' on line {line_num}"
))
})?;
let weight = E::default();
graph.add_edge(source_node, target_node, weight)?;
} else {
return Err(GraphError::Other(format!(
"Invalid edge element - missing source or target on line {line_num}"
)));
}
Ok(())
}
#[allow(dead_code)]
fn parse_digraph_edge_element<N, E>(
line: &str,
graph: &mut DiGraph<N, E>,
_keys: &HashMap<String, GraphMLKey>,
_weighted: bool,
line_num: usize,
) -> Result<()>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
{
let mut source_id = None;
let mut target_id = None;
if let Some(source_start) = line.find(r#"source=""#) {
let start = source_start + 8;
if let Some(end) = line[start..].find('"') {
source_id = Some(&line[start..start + end]);
}
}
if let Some(target_start) = line.find(r#"target=""#) {
let start = target_start + 8;
if let Some(end) = line[start..].find('"') {
target_id = Some(&line[start..start + end]);
}
}
if let (Some(source_id), Some(target_id)) = (source_id, target_id) {
let source_node = N::from_str(source_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse source node '{source_id}' on line {line_num}"
))
})?;
let target_node = N::from_str(target_id).map_err(|_| {
GraphError::Other(format!(
"Failed to parse target node '{target_id}' on line {line_num}"
))
})?;
let weight = E::default();
graph.add_edge(source_node, target_node, weight)?;
} else {
return Err(GraphError::Other(format!(
"Invalid edge element - missing source or target on line {line_num}"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_read_simple_graphml() {
let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
.expect("Test: failed to write to temp file");
writeln!(
temp_file,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <graph id="G" edgedefault="undirected">"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="1"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="2"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="3"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <edge source="1" target="2"/>"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <edge source="2" target="3"/>"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" </graph>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
temp_file.flush().expect("Test: failed to flush temp file");
let graph: Graph<i32, f64> =
read_graphml_format(temp_file.path(), false).expect("Test operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_read_directed_graphml() {
let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
.expect("Test: failed to write to temp file");
writeln!(
temp_file,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <graph id="G" edgedefault="directed">"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="1"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="2"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="3"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <edge source="1" target="2"/>"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <edge source="2" target="3"/>"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" </graph>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
temp_file.flush().expect("Test: failed to flush temp file");
let graph: DiGraph<i32, f64> =
read_graphml_format_digraph(temp_file.path(), false).expect("Test operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_write_read_roundtrip() {
let mut original_graph: Graph<i32, f64> = Graph::new();
original_graph
.add_edge(1i32, 2i32, 1.5f64)
.expect("Test: failed to add edge");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Test: failed to add edge");
let temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
write_graphml_format(&original_graph, temp_file.path(), false)
.expect("Test operation failed");
let read_graph: Graph<i32, f64> =
read_graphml_format(temp_file.path(), false).expect("Test operation failed");
assert_eq!(read_graph.node_count(), original_graph.node_count());
assert_eq!(read_graph.edge_count(), original_graph.edge_count());
}
#[test]
fn test_digraph_write_read_roundtrip() {
let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
original_graph
.add_edge(1i32, 2i32, 1.5f64)
.expect("Test: failed to add edge");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Test: failed to add edge");
let temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
write_graphml_format_digraph(&original_graph, temp_file.path(), false)
.expect("Test operation failed");
let read_graph: DiGraph<i32, f64> =
read_graphml_format_digraph(temp_file.path(), false).expect("Test operation failed");
assert_eq!(read_graph.node_count(), original_graph.node_count());
assert_eq!(read_graph.edge_count(), original_graph.edge_count());
}
#[test]
fn test_invalid_xml() {
let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
writeln!(temp_file, "<invalid>xml</invalid>").expect("Test: failed to write to temp file");
temp_file.flush().expect("Test: failed to flush temp file");
let result: Result<Graph<i32, f64>> = read_graphml_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_directed_mismatch() {
let mut temp_file = NamedTempFile::new().expect("Test: failed to create temp file");
writeln!(temp_file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)
.expect("Test: failed to write to temp file");
writeln!(
temp_file,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <graph id="G" edgedefault="directed">"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="1"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <node id="2"/>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#" <edge source="1" target="2"/>"#)
.expect("Test: failed to write to temp file");
writeln!(temp_file, r#" </graph>"#).expect("Test: failed to write to temp file");
writeln!(temp_file, r#"</graphml>"#).expect("Test: failed to write to temp file");
temp_file.flush().expect("Test: failed to flush temp file");
let result: Result<Graph<i32, f64>> = read_graphml_format(temp_file.path(), false);
assert!(result.is_err());
}
}