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().expect("Operation failed");
69//! writeln!(temp_file, r#"{{"directed": false, "nodes": [{{"id": "1"}}, {{"id": "2"}}], "edges": [{{"source": "1", "target": "2"}}]}}"#).expect("Operation failed");
70//! temp_file.flush().expect("Operation failed");
71//!
72//! // Read the graph
73//! let graph: Graph<i32, f64> = read_json_format(temp_file.path(), false).expect("Operation failed");
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().expect("Operation failed");
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        .expect("Test: operation failed");
471        temp_file.flush().expect("Operation failed");
472
473        let graph: Graph<i32, f64> =
474            read_json_format(temp_file.path(), false).expect("Operation failed");
475
476        assert_eq!(graph.node_count(), 3);
477        assert_eq!(graph.edge_count(), 2);
478    }
479
480    #[test]
481    fn test_read_json_directed() {
482        let mut temp_file = NamedTempFile::new().expect("Operation failed");
483        writeln!(
484            temp_file,
485            r#"{{
486                "directed": true,
487                "nodes": [
488                    {{"id": "1"}},
489                    {{"id": "2"}},
490                    {{"id": "3"}}
491                ],
492                "edges": [
493                    {{"source": "1", "target": "2"}},
494                    {{"source": "2", "target": "3"}}
495                ]
496            }}"#
497        )
498        .expect("Test: operation failed");
499        temp_file.flush().expect("Operation failed");
500
501        let graph: DiGraph<i32, f64> =
502            read_json_format_digraph(temp_file.path(), false).expect("Operation failed");
503
504        assert_eq!(graph.node_count(), 3);
505        assert_eq!(graph.edge_count(), 2);
506    }
507
508    #[test]
509    fn test_read_json_weighted() {
510        let mut temp_file = NamedTempFile::new().expect("Operation failed");
511        writeln!(
512            temp_file,
513            r#"{{
514                "directed": false,
515                "nodes": [
516                    {{"id": "1"}},
517                    {{"id": "2"}},
518                    {{"id": "3"}}
519                ],
520                "edges": [
521                    {{"source": "1", "target": "2", "weight": 1.5}},
522                    {{"source": "2", "target": "3", "weight": 2.0}}
523                ]
524            }}"#
525        )
526        .expect("Test: operation failed");
527        temp_file.flush().expect("Operation failed");
528
529        let graph: Graph<i32, f64> =
530            read_json_format(temp_file.path(), true).expect("Operation failed");
531
532        assert_eq!(graph.node_count(), 3);
533        assert_eq!(graph.edge_count(), 2);
534    }
535
536    #[test]
537    fn test_write_read_roundtrip() {
538        let mut original_graph: Graph<i32, f64> = Graph::new();
539        original_graph
540            .add_edge(1i32, 2i32, 1.5f64)
541            .expect("Operation failed");
542        original_graph
543            .add_edge(2i32, 3i32, 2.0f64)
544            .expect("Operation failed");
545
546        let temp_file = NamedTempFile::new().expect("Operation failed");
547        write_json_format(&original_graph, temp_file.path(), true).expect("Operation failed");
548
549        let read_graph: Graph<i32, f64> =
550            read_json_format(temp_file.path(), true).expect("Operation failed");
551
552        assert_eq!(read_graph.node_count(), original_graph.node_count());
553        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
554    }
555
556    #[test]
557    fn test_digraph_write_read_roundtrip() {
558        let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
559        original_graph
560            .add_edge(1i32, 2i32, 1.5f64)
561            .expect("Operation failed");
562        original_graph
563            .add_edge(2i32, 3i32, 2.0f64)
564            .expect("Operation failed");
565
566        let temp_file = NamedTempFile::new().expect("Operation failed");
567        write_json_format_digraph(&original_graph, temp_file.path(), true)
568            .expect("Operation failed");
569
570        let read_graph: DiGraph<i32, f64> =
571            read_json_format_digraph(temp_file.path(), true).expect("Operation failed");
572
573        assert_eq!(read_graph.node_count(), original_graph.node_count());
574        assert_eq!(read_graph.edge_count(), original_graph.edge_count());
575    }
576
577    #[test]
578    fn test_invalid_json() {
579        let mut temp_file = NamedTempFile::new().expect("Operation failed");
580        writeln!(temp_file, "{{invalid json").expect("Operation failed");
581        temp_file.flush().expect("Operation failed");
582
583        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
584        assert!(result.is_err());
585    }
586
587    #[test]
588    fn test_missing_node_reference() {
589        let mut temp_file = NamedTempFile::new().expect("Operation failed");
590        writeln!(
591            temp_file,
592            r#"{{
593                "directed": false,
594                "nodes": [
595                    {{"id": "1"}},
596                    {{"id": "2"}}
597                ],
598                "edges": [
599                    {{"source": "1", "target": "3"}}
600                ]
601            }}"#
602        )
603        .expect("Test: operation failed");
604        temp_file.flush().expect("Operation failed");
605
606        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
607        assert!(result.is_err());
608    }
609
610    #[test]
611    fn test_directed_graph_mismatch() {
612        let mut temp_file = NamedTempFile::new().expect("Operation failed");
613        writeln!(
614            temp_file,
615            r#"{{
616                "directed": true,
617                "nodes": [
618                    {{"id": "1"}},
619                    {{"id": "2"}}
620                ],
621                "edges": [
622                    {{"source": "1", "target": "2"}}
623                ]
624            }}"#
625        )
626        .expect("Test: operation failed");
627        temp_file.flush().expect("Operation failed");
628
629        // Try to read as undirected graph - should fail
630        let result: Result<Graph<i32, f64>> = read_json_format(temp_file.path(), false);
631        assert!(result.is_err());
632    }
633}