1use crate::cg::{CallGraph, Edge, Node};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashSet;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct JsonExportConfig {
14 pub exclude_isolated_nodes: bool,
16 pub pretty_print: bool,
18 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#[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#[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 fn to_json(&self, name: &str, config: &JsonExportConfig) -> String;
64
65 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 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 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}