greppy/trace/output/
json.rs

1//! JSON output formatter
2//!
3//! Provides machine-readable JSON output for:
4//! - Tooling integration
5//! - Editor plugins
6//! - Scripting and automation
7//!
8//! @module trace/output/json
9
10use super::{
11    DeadCodeResult, FlowResult, ImpactResult, ModuleResult, PatternResult, RefsResult, ScopeResult,
12    StatsResult, TraceFormatter, TraceResult,
13};
14
15// =============================================================================
16// FORMATTER IMPLEMENTATION
17// =============================================================================
18
19/// JSON formatter for machine-readable output
20pub struct JsonFormatter {
21    pretty: bool,
22}
23
24impl JsonFormatter {
25    /// Create a new JSON formatter with pretty printing
26    pub fn new() -> Self {
27        Self { pretty: true }
28    }
29
30    /// Create a compact JSON formatter (no pretty printing)
31    pub fn compact() -> Self {
32        Self { pretty: false }
33    }
34
35    /// Serialize to JSON string
36    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        // Verify it's valid JSON
137        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        // Verify it's valid JSON
181        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        // Compact JSON should not contain newlines (except in strings)
205        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        // Should be valid JSON even with special characters
223        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}