traverse_graph/
cg_json.rs

1//! JSON format export for Call Graphs.
2//!
3//! Provides functionality to convert a `CallGraph` into JSON format,
4//! suitable for programmatic consumption and tooling integration.
5
6use crate::cg::{CallGraph, Edge, Node};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashSet;
10
11/// Configuration options for JSON export.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct JsonExportConfig {
14    /// If true, nodes with no incoming or outgoing edges will be excluded from the output.
15    pub exclude_isolated_nodes: bool,
16    /// If true, the JSON output will be pretty-printed with indentation.
17    pub pretty_print: bool,
18    /// If true, additional metadata will be included in the output.
19    pub include_metadata: bool,
20}
21
22impl Default for JsonExportConfig {
23    fn default() -> Self {
24        Self {
25            exclude_isolated_nodes: false,
26            pretty_print: true,
27            include_metadata: true,
28        }
29    }
30}
31
32/// Represents the complete graph structure in JSON format
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct JsonGraph {
35    pub name: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub metadata: Option<JsonMetadata>,
38    pub nodes: Vec<Node>,
39    pub edges: Vec<Edge>,
40}
41
42/// Metadata about the graph
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct JsonMetadata {
45    pub node_count: usize,
46    pub edge_count: usize,
47    pub isolated_node_count: usize,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub generated_at: Option<String>,
50}
51
52pub trait CgToJson {
53    /// Exports the graph to JSON format using default serialization.
54    ///
55    /// # Arguments
56    ///
57    /// * `name` - The name of the graph in the JSON output.
58    /// * `config` - Configuration options for the export.
59    ///
60    /// # Returns
61    ///
62    /// A string containing the JSON representation of the graph.
63    fn to_json(&self, name: &str, config: &JsonExportConfig) -> String;
64
65    /// Exports the graph to JSON format as a serde_json::Value with custom formatters.
66    ///
67    /// # Arguments
68    ///
69    /// * `name` - The name of the graph.
70    /// * `config` - Configuration options for the export.
71    /// * `node_formatter` - A closure that takes a `&Node` and returns a custom JSON value.
72    /// * `edge_formatter` - A closure that takes an `&Edge` and returns a custom JSON value.
73    ///
74    /// # Returns
75    ///
76    /// A serde_json::Value containing the JSON representation of the graph.
77    fn to_json_with_formatters<NF, EF>(
78        &self,
79        name: &str,
80        config: &JsonExportConfig,
81        node_formatter: NF,
82        edge_formatter: EF,
83    ) -> Value
84    where
85        NF: Fn(&Node) -> Value,
86        EF: Fn(&Edge) -> Value;
87}
88
89impl CgToJson for CallGraph {
90    /// Default JSON export using direct serialization of Node and Edge structures.
91    fn to_json(&self, name: &str, config: &JsonExportConfig) -> String {
92        let json_value = self.to_json_with_formatters(
93            name,
94            config,
95            |node| serde_json::to_value(node).unwrap_or(Value::Null),
96            |edge| serde_json::to_value(edge).unwrap_or(Value::Null),
97        );
98
99        if config.pretty_print {
100            serde_json::to_string_pretty(&json_value).unwrap_or_else(|e| {
101                format!("{{\"error\": \"Failed to serialize to JSON: {}\"}}", e)
102            })
103        } else {
104            serde_json::to_string(&json_value).unwrap_or_else(|e| {
105                format!("{{\"error\": \"Failed to serialize to JSON: {}\"}}", e)
106            })
107        }
108    }
109
110    /// Generic JSON export allowing full customization via closures.
111    fn to_json_with_formatters<NF, EF>(
112        &self,
113        name: &str,
114        config: &JsonExportConfig,
115        node_formatter: NF,
116        edge_formatter: EF,
117    ) -> Value
118    where
119        NF: Fn(&Node) -> Value,
120        EF: Fn(&Edge) -> Value,
121    {
122        let connected_node_ids: Option<HashSet<usize>> = if config.exclude_isolated_nodes {
123            let mut ids = HashSet::new();
124            for edge in self.iter_edges() {
125                ids.insert(edge.source_node_id);
126                ids.insert(edge.target_node_id);
127            }
128            Some(ids)
129        } else {
130            None
131        };
132
133        let nodes: Vec<Value> = self
134            .iter_nodes()
135            .filter(|node| {
136                if let Some(ref connected_ids) = connected_node_ids {
137                    connected_ids.contains(&node.id)
138                } else {
139                    true
140                }
141            })
142            .map(&node_formatter)
143            .collect();
144
145        let edges: Vec<Value> = self.iter_edges().map(&edge_formatter).collect();
146
147        let mut graph = serde_json::json!({
148            "name": name,
149            "nodes": nodes,
150            "edges": edges,
151        });
152
153        if config.include_metadata {
154            let isolated_count = if config.exclude_isolated_nodes {
155                0
156            } else {
157                let connected_ids = {
158                    let mut ids = HashSet::new();
159                    for edge in self.iter_edges() {
160                        ids.insert(edge.source_node_id);
161                        ids.insert(edge.target_node_id);
162                    }
163                    ids
164                };
165                self.iter_nodes()
166                    .filter(|node| !connected_ids.contains(&node.id))
167                    .count()
168            };
169
170            let metadata = JsonMetadata {
171                node_count: nodes.len(),
172                edge_count: edges.len(),
173                isolated_node_count: isolated_count,
174                generated_at: Some(chrono::Utc::now().to_rfc3339()),
175            };
176
177            graph["metadata"] = serde_json::to_value(metadata).unwrap_or(Value::Null);
178        }
179
180        graph
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::cg::{CallGraph, EdgeType, NodeType, Visibility};
188    use serde_json::json;
189
190    fn create_test_graph() -> CallGraph {
191        let mut graph = CallGraph::new();
192        let n0 = graph.add_node(
193            "foo".to_string(),
194            NodeType::Function,
195            Some("ContractA".to_string()),
196            Visibility::Public,
197            (10, 20),
198        );
199        let n1 = graph.add_node(
200            "bar".to_string(),
201            NodeType::Function,
202            Some("ContractA".to_string()),
203            Visibility::Private,
204            (30, 40),
205        );
206        graph.add_edge(
207            n1,
208            n0,
209            EdgeType::Call,
210            (35, 38),
211            None,
212            1,
213            None,
214            None,
215            None,
216            None,
217        );
218        graph
219    }
220
221    #[test]
222    fn test_default_json_export() {
223        let graph = create_test_graph();
224        let config = JsonExportConfig::default();
225        let json_str = graph.to_json("Test Graph", &config);
226        
227        let json: Value = serde_json::from_str(&json_str).expect("Failed to parse JSON");
228        
229        assert_eq!(json["name"], "Test Graph");
230        assert!(json["metadata"].is_object());
231        assert_eq!(json["metadata"]["node_count"], 2);
232        assert_eq!(json["metadata"]["edge_count"], 1);
233        
234        assert!(json["nodes"].is_array());
235        assert_eq!(json["nodes"].as_array().unwrap().len(), 2);
236        
237        assert!(json["edges"].is_array());
238        assert_eq!(json["edges"].as_array().unwrap().len(), 1);
239    }
240
241    #[test]
242    fn test_exclude_isolated_nodes() {
243        let mut graph = CallGraph::new();
244        let n0 = graph.add_node(
245            "connected1".to_string(),
246            NodeType::Function,
247            Some("ContractA".to_string()),
248            Visibility::Public,
249            (10, 20),
250        );
251        let n1 = graph.add_node(
252            "connected2".to_string(),
253            NodeType::Function,
254            Some("ContractA".to_string()),
255            Visibility::Private,
256            (30, 40),
257        );
258        let _n2 = graph.add_node(
259            "isolated".to_string(),
260            NodeType::Function,
261            Some("ContractB".to_string()),
262            Visibility::Public,
263            (50, 60),
264        );
265        graph.add_edge(
266            n0, n1, EdgeType::Call, (15, 18), None, 1, None, None, None, None,
267        );
268
269        let config_exclude = JsonExportConfig {
270            exclude_isolated_nodes: true,
271            pretty_print: false,
272            include_metadata: true,
273        };
274        let json_str = graph.to_json("Test", &config_exclude);
275        let json: Value = serde_json::from_str(&json_str).expect("Failed to parse JSON");
276        
277        assert_eq!(json["nodes"].as_array().unwrap().len(), 2);
278        assert_eq!(json["metadata"]["node_count"], 2);
279        assert_eq!(json["metadata"]["isolated_node_count"], 0);
280
281        let config_include = JsonExportConfig {
282            exclude_isolated_nodes: false,
283            pretty_print: false,
284            include_metadata: true,
285        };
286        let json_str = graph.to_json("Test", &config_include);
287        let json: Value = serde_json::from_str(&json_str).expect("Failed to parse JSON");
288        
289        assert_eq!(json["nodes"].as_array().unwrap().len(), 3);
290        assert_eq!(json["metadata"]["node_count"], 3);
291        assert_eq!(json["metadata"]["isolated_node_count"], 1);
292    }
293
294    #[test]
295    fn test_custom_formatter_json_export() {
296        let graph = create_test_graph();
297        let config = JsonExportConfig {
298            exclude_isolated_nodes: false,
299            pretty_print: false,
300            include_metadata: false,
301        };
302        
303        let json_value = graph.to_json_with_formatters(
304            "Custom Test",
305            &config,
306            |node| {
307                json!({
308                    "id": node.id,
309                    "label": format!("{}.{}", 
310                        node.contract_name.as_deref().unwrap_or(""),
311                        node.name
312                    ),
313                    "custom_field": "custom_value"
314                })
315            },
316            |edge| {
317                json!({
318                    "from": edge.source_node_id,
319                    "to": edge.target_node_id,
320                    "label": "custom_edge"
321                })
322            },
323        );
324        
325        assert_eq!(json_value["name"], "Custom Test");
326        assert!(json_value["metadata"].is_null());
327        
328        let nodes = json_value["nodes"].as_array().unwrap();
329        assert_eq!(nodes.len(), 2);
330        assert_eq!(nodes[0]["custom_field"], "custom_value");
331        
332        let edges = json_value["edges"].as_array().unwrap();
333        assert_eq!(edges.len(), 1);
334        assert_eq!(edges[0]["label"], "custom_edge");
335    }
336}