use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
#[cfg(feature = "hgraph")]
use crate::hgraph::h_graph::HeterogeneousGraph;
#[cfg(feature = "hgraph")]
#[derive(Debug)]
pub enum CsvIoError {
IoError(io::Error),
InvalidFormat(String),
ParseError {
field: String,
value: String,
details: String,
},
}
#[cfg(feature = "hgraph")]
impl std::fmt::Display for CsvIoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CsvIoError::IoError(e) => write!(f, "I/O error: {}", e),
CsvIoError::InvalidFormat(msg) => write!(f, "Invalid CSV format: {}", msg),
CsvIoError::ParseError {
field,
value,
details,
} => write!(f, "Failed to parse {} '{}': {}", field, value, details),
}
}
}
#[cfg(feature = "hgraph")]
impl std::error::Error for CsvIoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CsvIoError::IoError(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "hgraph")]
impl From<io::Error> for CsvIoError {
fn from(err: io::Error) -> Self {
CsvIoError::IoError(err)
}
}
#[cfg(feature = "hgraph")]
pub type Result<T> = std::result::Result<T, CsvIoError>;
#[cfg(feature = "hgraph")]
pub trait CsvIO<W, N, E> {
fn save_to_csv(&self, nodes_file: &str, edges_file: &str) -> Result<()>
where
W: Copy + PartialEq + std::fmt::Display,
N: Clone + std::fmt::Debug + std::fmt::Display,
E: Clone + std::fmt::Debug + std::fmt::Display;
fn load_from_csv(nodes_file: &str, edges_file: &str, directed: bool) -> Result<Self>
where
Self: Sized,
W: Copy + PartialEq + Default + std::str::FromStr,
N: Clone + std::fmt::Debug + std::str::FromStr,
E: Clone + std::fmt::Debug + Default + std::str::FromStr,
<W as std::str::FromStr>::Err: std::fmt::Debug,
<N as std::str::FromStr>::Err: std::fmt::Debug,
<E as std::str::FromStr>::Err: std::fmt::Debug;
}
#[cfg(feature = "hgraph")]
impl<W, N, E> CsvIO<W, N, E> for HeterogeneousGraph<W, N, E>
where
W: Copy + PartialEq + std::fmt::Debug,
N: crate::hgraph::h_node::NodeType,
E: crate::hgraph::h_edge::EdgeType + Default,
{
fn save_to_csv(&self, nodes_file: &str, edges_file: &str) -> Result<()>
where
W: std::fmt::Display,
N: std::fmt::Display,
E: std::fmt::Display,
{
let mut nodes_writer = File::create(nodes_file).map_err(CsvIoError::IoError)?;
let mut node_attrs: Vec<String> = self
.nodes
.iter()
.flat_map(|(_, node)| node.attributes.keys())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.cloned()
.collect();
node_attrs.sort();
writeln!(nodes_writer, "node_id,data,{}", node_attrs.join(","))
.map_err(CsvIoError::IoError)?;
for (id, node) in self.nodes.iter() {
let attrs: Vec<String> = node_attrs
.iter()
.map(|key| {
node.attributes
.get(key)
.map_or("".to_string(), |v| v.to_string())
})
.collect();
writeln!(
nodes_writer,
"{},{},{}",
id,
node.data.as_string(),
attrs.join(",")
)
.map_err(CsvIoError::IoError)?;
}
let mut edges_writer = File::create(edges_file).map_err(CsvIoError::IoError)?;
let mut edge_attrs: Vec<String> = self
.edges
.iter()
.flat_map(|(_, edge)| edge.attributes.keys())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.cloned()
.collect();
edge_attrs.sort();
writeln!(
edges_writer,
"edge_id,from,to,weight,data,{}",
edge_attrs.join(",")
)
.map_err(CsvIoError::IoError)?;
for (id, edge) in self.edges.iter() {
let attrs: Vec<String> = edge_attrs
.iter()
.map(|key| {
edge.attributes
.get(key)
.map_or("".to_string(), |v| v.to_string())
})
.collect();
writeln!(
edges_writer,
"{},{},{},{},{},{}",
id,
edge.from,
edge.to,
edge.weight,
edge.data.as_string(),
attrs.join(",")
)
.map_err(CsvIoError::IoError)?;
}
Ok(())
}
fn load_from_csv(nodes_file: &str, edges_file: &str, directed: bool) -> Result<Self>
where
Self: Sized,
W: Default + std::str::FromStr,
N: std::str::FromStr,
E: std::str::FromStr,
<W as std::str::FromStr>::Err: std::fmt::Debug,
<N as std::str::FromStr>::Err: std::fmt::Debug,
<E as std::str::FromStr>::Err: std::fmt::Debug,
{
let mut graph = HeterogeneousGraph::new(directed);
let nodes_reader = BufReader::new(File::open(nodes_file).map_err(CsvIoError::IoError)?);
let mut lines = nodes_reader.lines();
let header = lines
.next()
.ok_or(CsvIoError::InvalidFormat("Empty nodes file".to_string()))?
.map_err(CsvIoError::IoError)?;
let attr_keys: Vec<&str> = header.split(',').skip(2).collect();
if header.is_empty() || !header.starts_with("node_id,data") {
return Err(CsvIoError::InvalidFormat(
"Nodes CSV must start with 'node_id,data'".to_string(),
));
}
for (line_num, line) in lines.enumerate() {
let line = line.map_err(CsvIoError::IoError)?;
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 2 {
return Err(CsvIoError::InvalidFormat(format!(
"Line {} in nodes CSV has too few fields",
line_num + 2
)));
}
let _node_id: usize = parts[0].parse().unwrap_or_default(); let data = parts[1].parse().map_err(|e| CsvIoError::ParseError {
field: "node data".to_string(),
value: parts[1].to_string(),
details: format!("{:?}", e),
})?;
let node = graph.add_node(data);
for (key, value) in attr_keys.iter().zip(parts.iter().skip(2)) {
if !value.is_empty() {
graph.set_node_attribute(node, key.to_string(), value.to_string());
}
}
}
let edges_reader = BufReader::new(File::open(edges_file).map_err(CsvIoError::IoError)?);
let mut lines = edges_reader.lines();
let header = lines
.next()
.ok_or(CsvIoError::InvalidFormat("Empty edges file".to_string()))?
.map_err(CsvIoError::IoError)?;
let attr_keys: Vec<&str> = header.split(',').skip(5).collect();
if header.is_empty() || !header.starts_with("edge_id,from,to,weight,data") {
return Err(CsvIoError::InvalidFormat(
"Edges CSV must start with 'edge_id,from,to,weight,data'".to_string(),
));
}
for (line_num, line) in lines.enumerate() {
let line = line.map_err(CsvIoError::IoError)?;
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 5 {
return Err(CsvIoError::InvalidFormat(format!(
"Line {} in edges CSV has too few fields",
line_num + 2
)));
}
let _edge_id: usize = parts[0].parse().unwrap_or_default(); let from: usize = parts[1].parse().unwrap_or_default();
let to: usize = parts[2].parse().unwrap_or_default();
let weight = parts[3].parse().map_err(|e| CsvIoError::ParseError {
field: "weight".to_string(),
value: parts[3].to_string(),
details: format!("{:?}", e),
})?;
let data = parts[4].parse().map_err(|e| CsvIoError::ParseError {
field: "edge data".to_string(),
value: parts[4].to_string(),
details: format!("{:?}", e),
})?;
let edge_id = graph.add_edge(from, to, weight, data).unwrap_or_default();
for (key, value) in attr_keys.iter().zip(parts.iter().skip(5)) {
if !value.is_empty() {
graph.set_edge_attribute(edge_id, key.to_string(), value.to_string());
}
}
}
Ok(graph)
}
}
#[cfg(test)]
#[cfg(feature = "hgraph")]
mod tests {
use super::*;
use crate::hgraph::h_edge::EdgeType;
use crate::hgraph::h_node::NodeType;
use std::fmt;
use std::str::FromStr;
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct TestNode(String);
impl NodeType for TestNode {
fn as_string(&self) -> String {
self.0.clone()
}
}
impl fmt::Display for TestNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for TestNode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(TestNode(s.to_string()))
}
}
#[derive(Clone, Debug, Default)]
struct TestEdge(String);
impl EdgeType for TestEdge {
fn as_string(&self) -> String {
self.0.clone()
}
}
impl fmt::Display for TestEdge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for TestEdge {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(TestEdge(s.to_string()))
}
}
#[test]
fn test_csv_io_basic() {
let mut graph = HeterogeneousGraph::<u32, TestNode, TestEdge>::new(false);
let n1 = graph.add_node(TestNode("A".to_string()));
let n2 = graph.add_node(TestNode("B".to_string()));
let edge_id = graph
.add_edge(n1, n2, 1, TestEdge("edge".to_string()))
.unwrap();
graph.set_node_attribute(n1, "color".to_string(), "red".to_string());
graph.set_edge_attribute(edge_id, "type".to_string(), "road".to_string());
assert!(graph
.save_to_csv("test_nodes_basic.csv", "test_edges_basic.csv")
.is_ok());
let loaded_graph = HeterogeneousGraph::<u32, TestNode, TestEdge>::load_from_csv(
"test_nodes_basic.csv",
"test_edges_basic.csv",
false,
)
.unwrap();
assert_eq!(graph.nodes.len(), loaded_graph.nodes.len());
assert_eq!(graph.edges.len(), loaded_graph.edges.len());
assert_eq!(
graph.get_node_data(n1).unwrap().as_string(),
loaded_graph.get_node_data(0).unwrap().as_string()
);
assert_eq!(
graph.get_edge_data_by_id(edge_id).unwrap().as_string(),
loaded_graph.get_edge_data_by_id(0).unwrap().as_string()
);
}
#[test]
fn test_csv_io_multigraph() {
let mut graph = HeterogeneousGraph::<f64, TestNode, TestEdge>::new(true);
let n1 = graph.add_node(TestNode("X".to_string()));
let n2 = graph.add_node(TestNode("Y".to_string()));
let edge1 = graph
.add_edge(n1, n2, 1.5, TestEdge("friend".to_string()))
.unwrap();
let edge2 = graph
.add_edge(n1, n2, 2.0, TestEdge("colleague".to_string()))
.unwrap();
graph.set_node_attribute(n1, "type".to_string(), "person".to_string());
graph.set_edge_attribute(edge1, "strength".to_string(), "strong".to_string());
graph.set_edge_attribute(edge2, "strength".to_string(), "weak".to_string());
assert!(graph
.save_to_csv("test_nodes_multi.csv", "test_edges_multi.csv")
.is_ok());
let loaded_graph = HeterogeneousGraph::<f64, TestNode, TestEdge>::load_from_csv(
"test_nodes_multi.csv",
"test_edges_multi.csv",
true,
)
.unwrap();
assert_eq!(graph.nodes.len(), loaded_graph.nodes.len());
assert_eq!(graph.edges.len(), loaded_graph.edges.len());
assert_eq!(loaded_graph.get_edges_between(0, 1).len(), 2);
}
#[test]
fn test_csv_io_invalid_file() {
let result = HeterogeneousGraph::<u32, TestNode, TestEdge>::load_from_csv(
"nonexistent_nodes.csv",
"nonexistent_edges.csv",
false,
);
assert!(matches!(result, Err(CsvIoError::IoError(_))));
std::fs::write("empty_nodes.csv", "").unwrap();
let result = HeterogeneousGraph::<u32, TestNode, TestEdge>::load_from_csv(
"empty_nodes.csv",
"nonexistent_edges.csv",
false,
);
assert!(matches!(result, Err(CsvIoError::InvalidFormat(_))));
}
}