scirs2_graph/io/
json.rs

1//! JSON format I/O for graphs
2//!
3//! This module provides functionality for reading and writing graphs in JSON format.
4//! The JSON format provides a flexible, human-readable representation of graph structures
5//! with support for arbitrary node and edge attributes.
6//!
7//! # Format Specification
8//!
9//! The JSON graph format uses the following structure:
10//! ```json
11//! {
12//!   "directed": false,
13//!   "nodes": [
14//!     {"id": "node1", "data": {...}},
15//!     {"id": "node2", "data": {...}}
16//!   ],
17//!   "edges": [
18//!     {"source": "node1", "target": "node2", "weight": 1.5},
19//!     {"source": "node2", "target": "node3", "weight": 2.0}
20//!   ]
21//! }
22//! ```
23//!
24//! # Examples
25//!
26//! ## Unweighted graph:
27//! ```json
28//! {
29//!   "directed": false,
30//!   "nodes": [
31//!     {"id": "1"},
32//!     {"id": "2"},
33//!     {"id": "3"}
34//!   ],
35//!   "edges": [
36//!     {"source": "1", "target": "2"},
37//!     {"source": "2", "target": "3"}
38//!   ]
39//! }
40//! ```
41//!
42//! ## Weighted graph:
43//! ```json
44//! {
45//!   "directed": true,
46//!   "nodes": [
47//!     {"id": "1", "label": "Start"},
48//!     {"id": "2", "label": "Middle"},
49//!     {"id": "3", "label": "End"}
50//!   ],
51//!   "edges": [
52//!     {"source": "1", "target": "2", "weight": 1.5},
53//!     {"source": "2", "target": "3", "weight": 2.0}
54//!   ]
55//! }
56//! ```
57//!
58//! # Usage
59//!
60//! ```rust
61//! use std::fs::File;
62//! use std::io::Write;
63//! use tempfile::NamedTempFile;
64//! use scirs2_graph::base::Graph;
65//! use scirs2_graph::io::json::{read_json_format, write_json_format};
66//!
67//! // Create a temporary file with JSON data
68//! let mut temp_file = NamedTempFile::new().unwrap();
69//! writeln!(temp_file, r#"{{"directed": false, "nodes": [{{"id": "1"}}, {{"id": "2"}}], "edges": [{{"source": "1", "target": "2"}}]}}"#).unwrap();
70//! temp_file.flush().unwrap();
71//!
72//! // Read the graph
73//! let graph: Graph<i32, f64> = read_json_format(temp_file.path(), false).unwrap();
74//! assert_eq!(graph.node_count(), 2);
75//! assert_eq!(graph.edge_count(), 1);
76//! ```
77
78use std::collections::HashMap;
79use std::fs::File;
80use std::io::{BufReader, Write};
81use std::path::Path;
82use std::str::FromStr;
83
84use serde::{Deserialize, Serialize};
85
86use crate::base::{DiGraph, EdgeWeight, Graph, Node};
87use crate::error::{GraphError, Result};
88
89/// JSON representation of a graph node
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct JsonNode {
92    /// Node identifier
93    pub id: String,
94    /// Optional node label
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub label: Option<String>,
97    /// Additional node data (currently unused but reserved for future extensions)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub data: Option<serde_json::Value>,
100}
101
102/// JSON representation of a graph edge
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct JsonEdge {
105    /// Source node identifier
106    pub source: String,
107    /// Target node identifier
108    pub target: String,
109    /// Optional edge weight
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub weight: Option<f64>,
112    /// Optional edge label
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub label: Option<String>,
115    /// Additional edge data (currently unused but reserved for future extensions)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub data: Option<serde_json::Value>,
118}
119
120/// JSON representation of a complete graph
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct JsonGraph {
123    /// Whether the graph is directed
124    #[serde(default = "default_directed")]
125    pub directed: bool,
126    /// List of nodes in the graph
127    pub nodes: Vec<JsonNode>,
128    /// List of edges in the graph
129    pub edges: Vec<JsonEdge>,
130    /// Optional graph metadata
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub metadata: Option<serde_json::Value>,
133}
134
135/// Default value for directed field
136#[allow(dead_code)]
137fn default_directed() -> bool {
138    false
139}
140
141/// Read an undirected graph from JSON format
142///
143/// # Arguments
144///
145/// * `path` - Path to the input file
146/// * `weighted` - Whether to parse edge weights from the JSON
147///
148/// # Returns
149///
150/// * `Ok(Graph)` - The graph read from the file
151/// * `Err(GraphError)` - If there was an error reading or parsing the file
152///
153/// # Format
154///
155/// The JSON format supports:
156/// - Node declarations with optional labels and data
157/// - Edge declarations with optional weights, labels, and data
158/// - Graph metadata and directedness specification
159/// - Flexible attribute storage for future extensions
160#[allow(dead_code)]
161pub fn read_json_format<N, E, P>(path: P, weighted: bool) -> Result<Graph<N, E>>
162where
163    N: Node + std::fmt::Debug + FromStr + Clone,
164    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
165    P: AsRef<Path>,
166{
167    let file = File::open(path)?;
168    let reader = BufReader::new(file);
169    let json_graph: JsonGraph = serde_json::from_reader(reader)
170        .map_err(|e| GraphError::Other(format!("Failed to parse JSON: {e}")))?;
171
172    // Verify this is an undirected graph
173    if json_graph.directed {
174        return Err(GraphError::Other(
175            "JSON file contains a directed graph, but undirected graph was requested".to_string(),
176        ));
177    }
178
179    let mut graph = Graph::new();
180
181    // Parse nodes first to ensure they exist
182    let mut node_map = HashMap::new();
183    for json_node in &json_graph.nodes {
184        let node = N::from_str(&json_node.id)
185            .map_err(|_| GraphError::Other(format!("Failed to parse node ID: {}", json_node.id)))?;
186        node_map.insert(json_node.id.clone(), node);
187    }
188
189    // Parse edges
190    for json_edge in &json_graph.edges {
191        let source_node = node_map.get(&json_edge.source).ok_or_else(|| {
192            GraphError::Other(format!(
193                "Edge references unknown source node: {}",
194                json_edge.source
195            ))
196        })?;
197
198        let target_node = node_map.get(&json_edge.target).ok_or_else(|| {
199            GraphError::Other(format!(
200                "Edge references unknown target node: {}",
201                json_edge.target
202            ))
203        })?;
204
205        // Parse weight if needed
206        let weight = if weighted {
207            if let Some(w) = json_edge.weight {
208                E::from_str(&w.to_string())
209                    .map_err(|_| GraphError::Other(format!("Failed to parse edge weight: {w}")))?
210            } else {
211                E::default()
212            }
213        } else {
214            E::default()
215        };
216
217        // Add edge
218        graph.add_edge(source_node.clone(), target_node.clone(), weight)?;
219    }
220
221    Ok(graph)
222}
223
224/// Read a directed graph from JSON format
225///
226/// # Arguments
227///
228/// * `path` - Path to the input file
229/// * `weighted` - Whether to parse edge weights from the JSON
230///
231/// # Returns
232///
233/// * `Ok(DiGraph)` - The directed graph read from the file
234/// * `Err(GraphError)` - If there was an error reading or parsing the file
235#[allow(dead_code)]
236pub fn read_json_format_digraph<N, E, P>(path: P, weighted: bool) -> Result<DiGraph<N, E>>
237where
238    N: Node + std::fmt::Debug + FromStr + Clone,
239    E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
240    P: AsRef<Path>,
241{
242    let file = File::open(path)?;
243    let reader = BufReader::new(file);
244    let json_graph: JsonGraph = serde_json::from_reader(reader)
245        .map_err(|e| GraphError::Other(format!("Failed to parse JSON: {e}")))?;
246
247    let mut graph = DiGraph::new();
248
249    // Parse nodes first to ensure they exist
250    let mut node_map = HashMap::new();
251    for json_node in &json_graph.nodes {
252        let node = N::from_str(&json_node.id)
253            .map_err(|_| GraphError::Other(format!("Failed to parse node ID: {}", json_node.id)))?;
254        node_map.insert(json_node.id.clone(), node);
255    }
256
257    // Parse edges
258    for json_edge in &json_graph.edges {
259        let source_node = node_map.get(&json_edge.source).ok_or_else(|| {
260            GraphError::Other(format!(
261                "Edge references unknown source node: {}",
262                json_edge.source
263            ))
264        })?;
265
266        let target_node = node_map.get(&json_edge.target).ok_or_else(|| {
267            GraphError::Other(format!(
268                "Edge references unknown target node: {}",
269                json_edge.target
270            ))
271        })?;
272
273        // Parse weight if needed
274        let weight = if weighted {
275            if let Some(w) = json_edge.weight {
276                E::from_str(&w.to_string())
277                    .map_err(|_| GraphError::Other(format!("Failed to parse edge weight: {w}")))?
278            } else {
279                E::default()
280            }
281        } else {
282            E::default()
283        };
284
285        // Add directed edge
286        graph.add_edge(source_node.clone(), target_node.clone(), weight)?;
287    }
288
289    Ok(graph)
290}
291
292/// Write an undirected graph to JSON format
293///
294/// # Arguments
295///
296/// * `graph` - The graph to write
297/// * `path` - Path to the output file
298/// * `weighted` - Whether to include edge weights in the output
299///
300/// # Returns
301///
302/// * `Ok(())` - If the graph was written successfully
303/// * `Err(GraphError)` - If there was an error writing the file
304#[allow(dead_code)]
305pub fn write_json_format<N, E, Ix, P>(
306    graph: &Graph<N, E, Ix>,
307    path: P,
308    weighted: bool,
309) -> Result<()>
310where
311    N: Node + std::fmt::Debug + std::fmt::Display + Clone,
312    E: EdgeWeight
313        + std::marker::Copy
314        + std::fmt::Debug
315        + std::default::Default
316        + std::fmt::Display
317        + Clone,
318    Ix: petgraph::graph::IndexType,
319    P: AsRef<Path>,
320{
321    let mut file = File::create(path)?;
322
323    // Collect nodes
324    let nodes: Vec<JsonNode> = graph
325        .nodes()
326        .iter()
327        .map(|node| JsonNode {
328            id: node.to_string(),
329            label: None,
330            data: None,
331        })
332        .collect();
333
334    // Collect edges
335    let edges: Vec<JsonEdge> = graph
336        .edges()
337        .iter()
338        .map(|edge| JsonEdge {
339            source: edge.source.to_string(),
340            target: edge.target.to_string(),
341            weight: if weighted {
342                // Convert weight to f64 through string parsing
343                edge.weight.to_string().parse::<f64>().ok()
344            } else {
345                None
346            },
347            label: None,
348            data: None,
349        })
350        .collect();
351
352    // Create JSON graph
353    let json_graph = JsonGraph {
354        directed: false,
355        nodes,
356        edges,
357        metadata: None,
358    };
359
360    // Write JSON to file
361    let json_string = serde_json::to_string_pretty(&json_graph)
362        .map_err(|e| GraphError::Other(format!("Failed to serialize JSON: {e}")))?;
363
364    write!(file, "{json_string}")?;
365
366    Ok(())
367}
368
369/// Write a directed graph to JSON format
370///
371/// # Arguments
372///
373/// * `graph` - The directed graph to write
374/// * `path` - Path to the output file
375/// * `weighted` - Whether to include edge weights in the output
376///
377/// # Returns
378///
379/// * `Ok(())` - If the graph was written successfully
380/// * `Err(GraphError)` - If there was an error writing the file
381#[allow(dead_code)]
382pub fn write_json_format_digraph<N, E, Ix, P>(
383    graph: &DiGraph<N, E, Ix>,
384    path: P,
385    weighted: bool,
386) -> Result<()>
387where
388    N: Node + std::fmt::Debug + std::fmt::Display + Clone,
389    E: EdgeWeight
390        + std::marker::Copy
391        + std::fmt::Debug
392        + std::default::Default
393        + std::fmt::Display
394        + Clone,
395    Ix: petgraph::graph::IndexType,
396    P: AsRef<Path>,
397{
398    let mut file = File::create(path)?;
399
400    // Collect nodes
401    let nodes: Vec<JsonNode> = graph
402        .nodes()
403        .iter()
404        .map(|node| JsonNode {
405            id: node.to_string(),
406            label: None,
407            data: None,
408        })
409        .collect();
410
411    // Collect edges
412    let edges: Vec<JsonEdge> = graph
413        .edges()
414        .iter()
415        .map(|edge| JsonEdge {
416            source: edge.source.to_string(),
417            target: edge.target.to_string(),
418            weight: if weighted {
419                // Convert weight to f64 through string parsing
420                edge.weight.to_string().parse::<f64>().ok()
421            } else {
422                None
423            },
424            label: None,
425            data: None,
426        })
427        .collect();
428
429    // Create JSON graph
430    let json_graph = JsonGraph {
431        directed: true,
432        nodes,
433        edges,
434        metadata: None,
435    };
436
437    // Write JSON to file
438    let json_string = serde_json::to_string_pretty(&json_graph)
439        .map_err(|e| GraphError::Other(format!("Failed to serialize JSON: {e}")))?;
440
441    write!(file, "{json_string}")?;
442
443    Ok(())
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use std::io::Write;
450    use tempfile::NamedTempFile;
451
452    #[test]
453    fn test_read_json_undirected() {
454        let mut temp_file = NamedTempFile::new().unwrap();
455        writeln!(
456            temp_file,
457            r#"{{
458                "directed": false,
459                "nodes": [
460                    {{"id": "1"}},
461                    {{"id": "2"}},
462                    {{"id": "3"}}
463                ],
464                "edges": [
465                    {{"source": "1", "target": "2"}},
466                    {{"source": "2", "target": "3"}}
467                ]
468            }}"#
469        )
470        .unwrap();
471        temp_file.flush().unwrap();
472
473        let graph: Graph<i32, f64> = read_json_format(temp_file.path(), false).unwrap();
474
475        assert_eq!(graph.node_count(), 3);
476        assert_eq!(graph.edge_count(), 2);
477    }
478
479    #[test]
480    fn test_read_json_directed() {
481        let mut temp_file = NamedTempFile::new().unwrap();
482        writeln!(
483            temp_file,
484            r#"{{
485                "directed": true,
486                "nodes": [
487                    {{"id": "1"}},
488                    {{"id": "2"}},
489                    {{"id": "3"}}
490                ],
491                "edges": [
492                    {{"source": "1", "target": "2"}},
493                    {{"source": "2", "target": "3"}}
494                ]
495            }}"#
496        )
497        .unwrap();
498        temp_file.flush().unwrap();
499
500        let graph: DiGraph<i32, f64> = read_json_format_digraph(temp_file.path(), false).unwrap();
501
502        assert_eq!(graph.node_count(), 3);
503        assert_eq!(graph.edge_count(), 2);
504    }
505
506    #[test]
507    fn test_read_json_weighted() {
508        let mut temp_file = NamedTempFile::new().unwrap();
509        writeln!(
510            temp_file,
511            r#"{{
512                "directed": false,
513                "nodes": [
514                    {{"id": "1"}},
515                    {{"id": "2"}},
516                    {{"id": "3"}}
517                ],
518                "edges": [
519                    {{"source": "1", "target": "2", "weight": 1.5}},
520                    {{"source": "2", "target": "3", "weight": 2.0}}
521                ]
522            }}"#
523        )
524        .unwrap();
525        temp_file.flush().unwrap();
526
527        let graph: Graph<i32, f64> = read_json_format(temp_file.path(), true).unwrap();
528
529        assert_eq!(graph.node_count(), 3);
530        assert_eq!(graph.edge_count(), 2);
531    }
532
533    #[test]
534    fn test_write_read_roundtrip() {
535        let mut original_graph: Graph<i32, f64> = Graph::new();
536        original_graph.add_edge(1i32, 2i32, 1.5f64).unwrap();
537        original_graph.add_edge(2i32, 3i32, 2.0f64).unwrap();
538
539        let temp_file = NamedTempFile::new().unwrap();
540        write_json_format(&original_graph, temp_file.path(), true).unwrap();
541
542        let read_graph: Graph<i32, f64> = read_json_format(temp_file.path(), true).unwrap();
543
544        assert_eq!(read_graph.node_count(), original_graph.node_count());
545        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
546    }
547
548    #[test]
549    fn test_digraph_write_read_roundtrip() {
550        let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
551        original_graph.add_edge(1i32, 2i32, 1.5f64).unwrap();
552        original_graph.add_edge(2i32, 3i32, 2.0f64).unwrap();
553
554        let temp_file = NamedTempFile::new().unwrap();
555        write_json_format_digraph(&original_graph, temp_file.path(), true).unwrap();
556
557        let read_graph: DiGraph<i32, f64> =
558            read_json_format_digraph(temp_file.path(), true).unwrap();
559
560        assert_eq!(read_graph.node_count(), original_graph.node_count());
561        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
562    }
563
564    #[test]
565    fn test_invalid_json() {
566        let mut temp_file = NamedTempFile::new().unwrap();
567        writeln!(temp_file, "{{invalid json").unwrap();
568        temp_file.flush().unwrap();
569
570        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
571        assert!(result.is_err());
572    }
573
574    #[test]
575    fn test_missing_node_reference() {
576        let mut temp_file = NamedTempFile::new().unwrap();
577        writeln!(
578            temp_file,
579            r#"{{
580                "directed": false,
581                "nodes": [
582                    {{"id": "1"}},
583                    {{"id": "2"}}
584                ],
585                "edges": [
586                    {{"source": "1", "target": "3"}}
587                ]
588            }}"#
589        )
590        .unwrap();
591        temp_file.flush().unwrap();
592
593        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
594        assert!(result.is_err());
595    }
596
597    #[test]
598    fn test_directed_graph_mismatch() {
599        let mut temp_file = NamedTempFile::new().unwrap();
600        writeln!(
601            temp_file,
602            r#"{{
603                "directed": true,
604                "nodes": [
605                    {{"id": "1"}},
606                    {{"id": "2"}}
607                ],
608                "edges": [
609                    {{"source": "1", "target": "2"}}
610                ]
611            }}"#
612        )
613        .unwrap();
614        temp_file.flush().unwrap();
615
616        // Try to read as undirected graph - should fail
617        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
618        assert!(result.is_err());
619    }
620}