use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::Path;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::base::{DiGraph, EdgeWeight, Graph, Node};
use crate::error::{GraphError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonNode {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonEdge {
pub source: String,
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonGraph {
#[serde(default = "default_directed")]
pub directed: bool,
pub nodes: Vec<JsonNode>,
pub edges: Vec<JsonEdge>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[allow(dead_code)]
fn default_directed() -> bool {
false
}
#[allow(dead_code)]
pub fn read_json_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 json_graph: JsonGraph = serde_json::from_reader(reader)
.map_err(|e| GraphError::Other(format!("Failed to parse JSON: {e}")))?;
if json_graph.directed {
return Err(GraphError::Other(
"JSON file contains a directed graph, but undirected graph was requested".to_string(),
));
}
let mut graph = Graph::new();
let mut node_map = HashMap::new();
for json_node in &json_graph.nodes {
let node = N::from_str(&json_node.id)
.map_err(|_| GraphError::Other(format!("Failed to parse node ID: {}", json_node.id)))?;
node_map.insert(json_node.id.clone(), node);
}
for json_edge in &json_graph.edges {
let source_node = node_map.get(&json_edge.source).ok_or_else(|| {
GraphError::Other(format!(
"Edge references unknown source node: {}",
json_edge.source
))
})?;
let target_node = node_map.get(&json_edge.target).ok_or_else(|| {
GraphError::Other(format!(
"Edge references unknown target node: {}",
json_edge.target
))
})?;
let weight = if weighted {
if let Some(w) = json_edge.weight {
E::from_str(&w.to_string())
.map_err(|_| GraphError::Other(format!("Failed to parse edge weight: {w}")))?
} else {
E::default()
}
} else {
E::default()
};
graph.add_edge(source_node.clone(), target_node.clone(), weight)?;
}
Ok(graph)
}
#[allow(dead_code)]
pub fn read_json_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 json_graph: JsonGraph = serde_json::from_reader(reader)
.map_err(|e| GraphError::Other(format!("Failed to parse JSON: {e}")))?;
let mut graph = DiGraph::new();
let mut node_map = HashMap::new();
for json_node in &json_graph.nodes {
let node = N::from_str(&json_node.id)
.map_err(|_| GraphError::Other(format!("Failed to parse node ID: {}", json_node.id)))?;
node_map.insert(json_node.id.clone(), node);
}
for json_edge in &json_graph.edges {
let source_node = node_map.get(&json_edge.source).ok_or_else(|| {
GraphError::Other(format!(
"Edge references unknown source node: {}",
json_edge.source
))
})?;
let target_node = node_map.get(&json_edge.target).ok_or_else(|| {
GraphError::Other(format!(
"Edge references unknown target node: {}",
json_edge.target
))
})?;
let weight = if weighted {
if let Some(w) = json_edge.weight {
E::from_str(&w.to_string())
.map_err(|_| GraphError::Other(format!("Failed to parse edge weight: {w}")))?
} else {
E::default()
}
} else {
E::default()
};
graph.add_edge(source_node.clone(), target_node.clone(), weight)?;
}
Ok(graph)
}
#[allow(dead_code)]
pub fn write_json_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)?;
let nodes: Vec<JsonNode> = graph
.nodes()
.iter()
.map(|node| JsonNode {
id: node.to_string(),
label: None,
data: None,
})
.collect();
let edges: Vec<JsonEdge> = graph
.edges()
.iter()
.map(|edge| JsonEdge {
source: edge.source.to_string(),
target: edge.target.to_string(),
weight: if weighted {
edge.weight.to_string().parse::<f64>().ok()
} else {
None
},
label: None,
data: None,
})
.collect();
let json_graph = JsonGraph {
directed: false,
nodes,
edges,
metadata: None,
};
let json_string = serde_json::to_string_pretty(&json_graph)
.map_err(|e| GraphError::Other(format!("Failed to serialize JSON: {e}")))?;
write!(file, "{json_string}")?;
Ok(())
}
#[allow(dead_code)]
pub fn write_json_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)?;
let nodes: Vec<JsonNode> = graph
.nodes()
.iter()
.map(|node| JsonNode {
id: node.to_string(),
label: None,
data: None,
})
.collect();
let edges: Vec<JsonEdge> = graph
.edges()
.iter()
.map(|edge| JsonEdge {
source: edge.source.to_string(),
target: edge.target.to_string(),
weight: if weighted {
edge.weight.to_string().parse::<f64>().ok()
} else {
None
},
label: None,
data: None,
})
.collect();
let json_graph = JsonGraph {
directed: true,
nodes,
edges,
metadata: None,
};
let json_string = serde_json::to_string_pretty(&json_graph)
.map_err(|e| GraphError::Other(format!("Failed to serialize JSON: {e}")))?;
write!(file, "{json_string}")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_read_json_undirected() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(
temp_file,
r#"{{
"directed": false,
"nodes": [
{{"id": "1"}},
{{"id": "2"}},
{{"id": "3"}}
],
"edges": [
{{"source": "1", "target": "2"}},
{{"source": "2", "target": "3"}}
]
}}"#
)
.expect("Test: operation failed");
temp_file.flush().expect("Operation failed");
let graph: Graph<i32, f64> =
read_json_format(temp_file.path(), false).expect("Operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_read_json_directed() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(
temp_file,
r#"{{
"directed": true,
"nodes": [
{{"id": "1"}},
{{"id": "2"}},
{{"id": "3"}}
],
"edges": [
{{"source": "1", "target": "2"}},
{{"source": "2", "target": "3"}}
]
}}"#
)
.expect("Test: operation failed");
temp_file.flush().expect("Operation failed");
let graph: DiGraph<i32, f64> =
read_json_format_digraph(temp_file.path(), false).expect("Operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_read_json_weighted() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(
temp_file,
r#"{{
"directed": false,
"nodes": [
{{"id": "1"}},
{{"id": "2"}},
{{"id": "3"}}
],
"edges": [
{{"source": "1", "target": "2", "weight": 1.5}},
{{"source": "2", "target": "3", "weight": 2.0}}
]
}}"#
)
.expect("Test: operation failed");
temp_file.flush().expect("Operation failed");
let graph: Graph<i32, f64> =
read_json_format(temp_file.path(), true).expect("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("Operation failed");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Operation failed");
let temp_file = NamedTempFile::new().expect("Operation failed");
write_json_format(&original_graph, temp_file.path(), true).expect("Operation failed");
let read_graph: Graph<i32, f64> =
read_json_format(temp_file.path(), true).expect("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("Operation failed");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Operation failed");
let temp_file = NamedTempFile::new().expect("Operation failed");
write_json_format_digraph(&original_graph, temp_file.path(), true)
.expect("Operation failed");
let read_graph: DiGraph<i32, f64> =
read_json_format_digraph(temp_file.path(), true).expect("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_json() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(temp_file, "{{invalid json").expect("Operation failed");
temp_file.flush().expect("Operation failed");
let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_missing_node_reference() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(
temp_file,
r#"{{
"directed": false,
"nodes": [
{{"id": "1"}},
{{"id": "2"}}
],
"edges": [
{{"source": "1", "target": "3"}}
]
}}"#
)
.expect("Test: operation failed");
temp_file.flush().expect("Operation failed");
let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_directed_graph_mismatch() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(
temp_file,
r#"{{
"directed": true,
"nodes": [
{{"id": "1"}},
{{"id": "2"}}
],
"edges": [
{{"source": "1", "target": "2"}}
]
}}"#
)
.expect("Test: operation failed");
temp_file.flush().expect("Operation failed");
let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
assert!(result.is_err());
}
}