1use super::{
11 DeadCodeResult, FlowResult, ImpactResult, ModuleResult, PatternResult, RefsResult, ScopeResult,
12 StatsResult, TraceFormatter, TraceResult,
13};
14
15pub struct JsonFormatter {
21 pretty: bool,
22}
23
24impl JsonFormatter {
25 pub fn new() -> Self {
27 Self { pretty: true }
28 }
29
30 pub fn compact() -> Self {
32 Self { pretty: false }
33 }
34
35 fn to_json<T: serde::Serialize>(&self, value: &T) -> String {
37 if self.pretty {
38 serde_json::to_string_pretty(value)
39 .unwrap_or_else(|e| format!(r#"{{"error": "JSON serialization failed: {}"}}"#, e))
40 } else {
41 serde_json::to_string(value)
42 .unwrap_or_else(|e| format!(r#"{{"error": "JSON serialization failed: {}"}}"#, e))
43 }
44 }
45}
46
47impl Default for JsonFormatter {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl TraceFormatter for JsonFormatter {
54 fn format_trace(&self, result: &TraceResult) -> String {
55 self.to_json(result)
56 }
57
58 fn format_refs(&self, result: &RefsResult) -> String {
59 self.to_json(result)
60 }
61
62 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
63 self.to_json(result)
64 }
65
66 fn format_flow(&self, result: &FlowResult) -> String {
67 self.to_json(result)
68 }
69
70 fn format_impact(&self, result: &ImpactResult) -> String {
71 self.to_json(result)
72 }
73
74 fn format_module(&self, result: &ModuleResult) -> String {
75 self.to_json(result)
76 }
77
78 fn format_pattern(&self, result: &PatternResult) -> String {
79 self.to_json(result)
80 }
81
82 fn format_scope(&self, result: &ScopeResult) -> String {
83 self.to_json(result)
84 }
85
86 fn format_stats(&self, result: &StatsResult) -> String {
87 self.to_json(result)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::trace::output::{ChainStep, InvocationPath, ReferenceInfo, ReferenceKind};
95
96 #[test]
97 fn test_format_trace_json() {
98 let formatter = JsonFormatter::new();
99 let result = TraceResult {
100 symbol: "validateUser".to_string(),
101 defined_at: Some("utils/validation.ts:8".to_string()),
102 kind: "function".to_string(),
103 invocation_paths: vec![InvocationPath {
104 entry_point: "POST /api/auth/login".to_string(),
105 entry_kind: "route".to_string(),
106 chain: vec![
107 ChainStep {
108 symbol: "loginController.handle".to_string(),
109 file: "auth.controller.ts".to_string(),
110 line: 8,
111 column: Some(5),
112 context: None,
113 },
114 ChainStep {
115 symbol: "authService.login".to_string(),
116 file: "auth.service.ts".to_string(),
117 line: 42,
118 column: Some(10),
119 context: None,
120 },
121 ChainStep {
122 symbol: "validateUser".to_string(),
123 file: "validation.ts".to_string(),
124 line: 8,
125 column: Some(3),
126 context: None,
127 },
128 ],
129 }],
130 total_paths: 47,
131 entry_points: 12,
132 };
133
134 let output = formatter.format_trace(&result);
135
136 let parsed: serde_json::Value =
138 serde_json::from_str(&output).expect("Should be valid JSON");
139
140 assert_eq!(parsed["symbol"], "validateUser");
141 assert_eq!(parsed["defined_at"], "utils/validation.ts:8");
142 assert_eq!(parsed["total_paths"], 47);
143 assert_eq!(parsed["entry_points"], 12);
144 assert_eq!(
145 parsed["invocation_paths"][0]["entry_point"],
146 "POST /api/auth/login"
147 );
148 assert_eq!(
149 parsed["invocation_paths"][0]["chain"][0]["symbol"],
150 "loginController.handle"
151 );
152 }
153
154 #[test]
155 fn test_format_refs_json() {
156 let formatter = JsonFormatter::new();
157 let mut by_kind = std::collections::HashMap::new();
158 by_kind.insert("read".to_string(), 5);
159 by_kind.insert("write".to_string(), 2);
160
161 let result = RefsResult {
162 symbol: "userId".to_string(),
163 defined_at: Some("types.ts:5".to_string()),
164 symbol_kind: Some("variable".to_string()),
165 references: vec![ReferenceInfo {
166 file: "handler.ts".to_string(),
167 line: 10,
168 column: 15,
169 kind: ReferenceKind::Read,
170 context: "const id = userId;".to_string(),
171 enclosing_symbol: Some("handleRequest".to_string()),
172 }],
173 total_refs: 7,
174 by_kind,
175 by_file: std::collections::HashMap::new(),
176 };
177
178 let output = formatter.format_refs(&result);
179
180 let parsed: serde_json::Value =
182 serde_json::from_str(&output).expect("Should be valid JSON");
183
184 assert_eq!(parsed["symbol"], "userId");
185 assert_eq!(parsed["total_refs"], 7);
186 assert_eq!(parsed["references"][0]["kind"], "read");
187 assert_eq!(parsed["references"][0]["line"], 10);
188 }
189
190 #[test]
191 fn test_compact_json() {
192 let formatter = JsonFormatter::compact();
193 let result = TraceResult {
194 symbol: "test".to_string(),
195 defined_at: None,
196 kind: "function".to_string(),
197 invocation_paths: vec![],
198 total_paths: 0,
199 entry_points: 0,
200 };
201
202 let output = formatter.format_trace(&result);
203
204 assert!(!output.contains("\n "));
206 }
207
208 #[test]
209 fn test_json_special_characters() {
210 let formatter = JsonFormatter::new();
211 let result = TraceResult {
212 symbol: "test\"with\\quotes".to_string(),
213 defined_at: Some("path/with spaces/file.ts:1".to_string()),
214 kind: "function".to_string(),
215 invocation_paths: vec![],
216 total_paths: 0,
217 entry_points: 0,
218 };
219
220 let output = formatter.format_trace(&result);
221
222 let parsed: serde_json::Value =
224 serde_json::from_str(&output).expect("Should be valid JSON");
225 assert!(parsed["symbol"].as_str().unwrap().contains("quotes"));
226 }
227}