1use std::path::Path;
4
5use super::types::{DiffImpactResult, ImpactResult};
6
7pub fn impact_to_json(result: &ImpactResult, root: &Path) -> serde_json::Value {
9 let callers_json: Vec<_> = result
10 .callers
11 .iter()
12 .map(|c| {
13 let rel = crate::rel_display(&c.file, root);
14 serde_json::json!({
15 "name": c.name,
16 "file": rel,
17 "line": c.line,
18 "call_line": c.call_line,
19 "snippet": c.snippet,
20 })
21 })
22 .collect();
23
24 let tests_json: Vec<_> = result.tests.iter().map(|t| t.to_json(root)).collect();
25
26 let mut output = serde_json::json!({
27 "function": result.function_name,
28 "callers": callers_json,
29 "tests": tests_json,
30 "caller_count": callers_json.len(),
31 "test_count": tests_json.len(),
32 });
33
34 if !result.transitive_callers.is_empty() {
35 let trans_json: Vec<_> = result
36 .transitive_callers
37 .iter()
38 .map(|c| {
39 let rel = crate::rel_display(&c.file, root);
40 serde_json::json!({
41 "name": c.name,
42 "file": rel,
43 "line": c.line,
44 "depth": c.depth,
45 })
46 })
47 .collect();
48 if let Some(obj) = output.as_object_mut() {
49 obj.insert("transitive_callers".into(), serde_json::json!(trans_json));
50 }
51 }
52
53 if result.degraded {
54 if let Some(obj) = output.as_object_mut() {
55 obj.insert("degraded".into(), serde_json::json!(true));
56 }
57 }
58
59 let type_json: Vec<_> = result
61 .type_impacted
62 .iter()
63 .map(|ti| {
64 let rel = crate::rel_display(&ti.file, root);
65 serde_json::json!({
66 "name": ti.name,
67 "file": rel,
68 "line": ti.line,
69 "shared_types": ti.shared_types,
70 })
71 })
72 .collect();
73 if let Some(obj) = output.as_object_mut() {
74 obj.insert("type_impacted".into(), serde_json::json!(type_json));
75 obj.insert(
76 "type_impacted_count".into(),
77 serde_json::json!(type_json.len()),
78 );
79 }
80
81 output
82}
83
84pub fn impact_to_mermaid(result: &ImpactResult, root: &Path) -> String {
86 let mut lines = vec!["graph TD".to_string()];
87 lines.push(format!(
88 " A[\"{}\"]\n style A fill:#f96",
89 mermaid_escape(&result.function_name)
90 ));
91
92 let mut idx = 1;
93 for c in &result.callers {
94 let rel = crate::rel_display(&c.file, root);
95 let letter = node_letter(idx);
96 lines.push(format!(
97 " {}[\"{} ({}:{})\"]",
98 letter,
99 mermaid_escape(&c.name),
100 mermaid_escape(&rel),
101 c.line
102 ));
103 lines.push(format!(" {} --> A", letter));
104 idx += 1;
105 }
106
107 for t in &result.tests {
108 let rel = crate::rel_display(&t.file, root);
109 let letter = node_letter(idx);
110 lines.push(format!(
111 " {}{{\"{}\\n{}\\ndepth: {}\"}}",
112 letter,
113 mermaid_escape(&t.name),
114 mermaid_escape(&rel),
115 t.call_depth
116 ));
117 lines.push(format!(" {} -.-> A", letter));
118 idx += 1;
119 }
120
121 for ti in &result.type_impacted {
122 let rel = crate::rel_display(&ti.file, root);
123 let letter = node_letter(idx);
124 let types_str = ti.shared_types.join(", ");
125 lines.push(format!(
126 " {}[/\"{} ({}:{})\\nvia: {}\"/]",
127 letter,
128 mermaid_escape(&ti.name),
129 mermaid_escape(&rel),
130 ti.line,
131 mermaid_escape(&types_str),
132 ));
133 lines.push(format!(" {} -. type .-> A", letter));
134 lines.push(format!(" style {} fill:#9cf", letter));
135 idx += 1;
136 }
137
138 lines.join("\n")
139}
140
141pub fn diff_impact_to_json(result: &DiffImpactResult, root: &Path) -> serde_json::Value {
143 let changed_json: Vec<_> = result
144 .changed_functions
145 .iter()
146 .map(|f| {
147 serde_json::json!({
148 "name": f.name,
149 "file": f.file.display().to_string(),
150 "line_start": f.line_start,
151 })
152 })
153 .collect();
154
155 let callers_json: Vec<_> = result
156 .all_callers
157 .iter()
158 .map(|c| {
159 let rel = crate::rel_display(&c.file, root);
160 serde_json::json!({
161 "name": c.name,
162 "file": rel,
163 "line": c.line,
164 "call_line": c.call_line,
165 })
166 })
167 .collect();
168
169 let tests_json: Vec<_> = result
170 .all_tests
171 .iter()
172 .map(|t| {
173 let rel = crate::rel_display(&t.file, root);
174 serde_json::json!({
175 "name": t.name,
176 "file": rel,
177 "line": t.line,
178 "via": t.via,
179 "call_depth": t.call_depth,
180 })
181 })
182 .collect();
183
184 serde_json::json!({
185 "changed_functions": changed_json,
186 "callers": callers_json,
187 "tests": tests_json,
188 "summary": {
189 "changed_count": result.summary.changed_count,
190 "caller_count": result.summary.caller_count,
191 "test_count": result.summary.test_count,
192 }
193 })
194}
195
196fn node_letter(mut i: usize) -> String {
201 let mut result = String::new();
202 loop {
203 result.insert(0, (b'A' + (i % 26) as u8) as char);
204 if i < 26 {
205 break;
206 }
207 i = i / 26 - 1;
208 }
209 result
210}
211
212fn mermaid_escape(s: &str) -> String {
213 s.replace('"', """)
214 .replace('<', "<")
215 .replace('>', ">")
216}
217
218#[cfg(test)]
219mod tests {
220 use super::super::types::*;
221 use super::*;
222 use std::path::PathBuf;
223
224 #[test]
227 fn test_node_letter_single_char() {
228 assert_eq!(node_letter(0), "A");
229 assert_eq!(node_letter(1), "B");
230 assert_eq!(node_letter(25), "Z");
231 }
232
233 #[test]
234 fn test_node_letter_double_char() {
235 assert_eq!(node_letter(26), "AA");
236 assert_eq!(node_letter(27), "AB");
237 assert_eq!(node_letter(51), "AZ");
238 assert_eq!(node_letter(52), "BA");
239 }
240
241 #[test]
242 fn test_node_letter_triple_char() {
243 assert_eq!(node_letter(702), "AAA");
244 }
245
246 #[test]
249 fn test_mermaid_escape_quotes() {
250 assert_eq!(mermaid_escape("hello \"world\""), "hello "world"");
251 }
252
253 #[test]
254 fn test_mermaid_escape_angle_brackets() {
255 assert_eq!(mermaid_escape("Vec<T>"), "Vec<T>");
256 }
257
258 #[test]
259 fn test_mermaid_escape_no_special() {
260 assert_eq!(mermaid_escape("plain_text"), "plain_text");
261 }
262
263 #[test]
264 fn test_mermaid_escape_all_special() {
265 assert_eq!(mermaid_escape("\"<>\""), ""<>"");
266 }
267
268 #[test]
271 fn test_impact_to_json_structure() {
272 let result = ImpactResult {
273 function_name: "target_fn".to_string(),
274 callers: vec![CallerDetail {
275 name: "caller_a".to_string(),
276 file: PathBuf::from("/project/src/lib.rs"),
277 line: 10,
278 call_line: 15,
279 snippet: Some("target_fn()".to_string()),
280 }],
281 tests: vec![TestInfo {
282 name: "test_target".to_string(),
283 file: PathBuf::from("/project/tests/test.rs"),
284 line: 1,
285 call_depth: 2,
286 }],
287 transitive_callers: Vec::new(),
288 type_impacted: Vec::new(),
289 degraded: false,
290 };
291 let root = Path::new("/project");
292 let json = impact_to_json(&result, root);
293
294 assert_eq!(json["function"], "target_fn");
295 assert_eq!(json["caller_count"], 1);
296 assert_eq!(json["test_count"], 1);
297
298 let callers = json["callers"].as_array().unwrap();
299 assert_eq!(callers[0]["name"], "caller_a");
300 assert_eq!(callers[0]["file"], "src/lib.rs");
301 assert_eq!(callers[0]["line"], 10);
302 assert_eq!(callers[0]["call_line"], 15);
303 assert_eq!(callers[0]["snippet"], "target_fn()");
304
305 let tests = json["tests"].as_array().unwrap();
306 assert_eq!(tests[0]["name"], "test_target");
307 assert_eq!(tests[0]["call_depth"], 2);
308 }
309
310 #[test]
311 fn test_impact_to_json_with_transitive() {
312 let result = ImpactResult {
313 function_name: "target".to_string(),
314 callers: Vec::new(),
315 tests: Vec::new(),
316 transitive_callers: vec![TransitiveCaller {
317 name: "indirect".to_string(),
318 file: PathBuf::from("/project/src/app.rs"),
319 line: 5,
320 depth: 2,
321 }],
322 type_impacted: Vec::new(),
323 degraded: false,
324 };
325 let root = Path::new("/project");
326 let json = impact_to_json(&result, root);
327
328 assert!(json["transitive_callers"].is_array());
329 let trans = json["transitive_callers"].as_array().unwrap();
330 assert_eq!(trans.len(), 1);
331 assert_eq!(trans[0]["name"], "indirect");
332 assert_eq!(trans[0]["depth"], 2);
333 }
334
335 #[test]
336 fn test_impact_to_json_empty() {
337 let result = ImpactResult {
338 function_name: "lonely".to_string(),
339 callers: Vec::new(),
340 tests: Vec::new(),
341 transitive_callers: Vec::new(),
342 type_impacted: Vec::new(),
343 degraded: false,
344 };
345 let root = Path::new("/project");
346 let json = impact_to_json(&result, root);
347
348 assert_eq!(json["function"], "lonely");
349 assert_eq!(json["caller_count"], 0);
350 assert_eq!(json["test_count"], 0);
351 assert!(json.get("transitive_callers").is_none());
352 assert_eq!(json["type_impacted"].as_array().unwrap().len(), 0);
353 assert_eq!(json["type_impacted_count"], 0);
354 }
355
356 #[test]
359 fn test_diff_impact_to_json_structure() {
360 let result = DiffImpactResult {
361 changed_functions: vec![ChangedFunction {
362 name: "changed_fn".to_string(),
363 file: PathBuf::from("src/lib.rs"),
364 line_start: 10,
365 }],
366 all_callers: vec![CallerDetail {
367 name: "caller_a".to_string(),
368 file: PathBuf::from("/project/src/app.rs"),
369 line: 20,
370 call_line: 25,
371 snippet: None,
372 }],
373 all_tests: vec![DiffTestInfo {
374 name: "test_changed".to_string(),
375 file: PathBuf::from("/project/tests/test.rs"),
376 line: 1,
377 via: "changed_fn".to_string(),
378 call_depth: 1,
379 }],
380 summary: DiffImpactSummary {
381 changed_count: 1,
382 caller_count: 1,
383 test_count: 1,
384 },
385 };
386 let root = Path::new("/project");
387 let json = diff_impact_to_json(&result, root);
388
389 let changed = json["changed_functions"].as_array().unwrap();
390 assert_eq!(changed.len(), 1);
391 assert_eq!(changed[0]["name"], "changed_fn");
392
393 let callers = json["callers"].as_array().unwrap();
394 assert_eq!(callers.len(), 1);
395 assert_eq!(callers[0]["name"], "caller_a");
396
397 let tests = json["tests"].as_array().unwrap();
398 assert_eq!(tests.len(), 1);
399 assert_eq!(tests[0]["name"], "test_changed");
400 assert_eq!(tests[0]["via"], "changed_fn");
401 assert_eq!(tests[0]["call_depth"], 1);
402
403 assert_eq!(json["summary"]["changed_count"], 1);
404 assert_eq!(json["summary"]["caller_count"], 1);
405 assert_eq!(json["summary"]["test_count"], 1);
406 }
407
408 #[test]
409 fn test_diff_impact_to_json_empty() {
410 let result = DiffImpactResult {
411 changed_functions: Vec::new(),
412 all_callers: Vec::new(),
413 all_tests: Vec::new(),
414 summary: DiffImpactSummary {
415 changed_count: 0,
416 caller_count: 0,
417 test_count: 0,
418 },
419 };
420 let root = Path::new("/project");
421 let json = diff_impact_to_json(&result, root);
422
423 assert_eq!(json["changed_functions"].as_array().unwrap().len(), 0);
424 assert_eq!(json["callers"].as_array().unwrap().len(), 0);
425 assert_eq!(json["tests"].as_array().unwrap().len(), 0);
426 assert_eq!(json["summary"]["changed_count"], 0);
427 }
428}