Skip to main content

cai_output/
formats.rs

1//! Output format implementations
2
3use crate::formatter::Truncate;
4use crate::{Formatter, FormatterConfig};
5use cai_core::Entry;
6use cai_core::Result;
7use std::io::Write;
8
9/// JSON array formatter
10#[derive(Debug, Clone, Default)]
11pub struct JsonFormatter {
12    config: FormatterConfig,
13}
14
15impl JsonFormatter {
16    /// Create a new formatter instance
17    pub fn new() -> Self {
18        Self::default()
19    }
20}
21
22impl Formatter for JsonFormatter {
23    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
24        serde_json::to_writer(writer, entries)?;
25        Ok(())
26    }
27
28    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
29        serde_json::to_writer(&mut *writer, entry)?;
30        writeln!(writer)?;
31        Ok(())
32    }
33
34    fn config(&self) -> &FormatterConfig {
35        &self.config
36    }
37
38    fn set_config(&mut self, config: FormatterConfig) {
39        self.config = config;
40    }
41}
42
43/// JSON Lines (newline-delimited JSON) formatter
44#[derive(Debug, Clone, Default)]
45pub struct JsonlFormatter {
46    config: FormatterConfig,
47}
48
49impl JsonlFormatter {
50    /// Create a new formatter instance
51    pub fn new() -> Self {
52        Self::default()
53    }
54}
55
56impl Formatter for JsonlFormatter {
57    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
58        for entry in entries {
59            self.format_one(entry, writer)?;
60        }
61        Ok(())
62    }
63
64    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
65        serde_json::to_writer(&mut *writer, entry)?;
66        writeln!(writer)?;
67        Ok(())
68    }
69
70    fn config(&self) -> &FormatterConfig {
71        &self.config
72    }
73
74    fn set_config(&mut self, config: FormatterConfig) {
75        self.config = config;
76    }
77}
78
79/// CSV formatter
80#[derive(Debug, Clone, Default)]
81pub struct CsvFormatter {
82    config: FormatterConfig,
83}
84
85impl CsvFormatter {
86    /// Create a new formatter instance
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Escape CSV fields containing quotes or commas
92    fn escape_field(value: &str) -> String {
93        if value.contains(',') || value.contains('"') || value.contains('\n') {
94            format!("\"{}\"", value.replace('"', "\"\""))
95        } else {
96            value.to_string()
97        }
98    }
99}
100
101impl Formatter for CsvFormatter {
102    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
103        // Write header
104        writeln!(writer, "id,source,timestamp,prompt,response")?;
105
106        for entry in entries {
107            self.format_one(entry, writer)?;
108        }
109        Ok(())
110    }
111
112    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
113        writeln!(
114            writer,
115            "{},{},{},{},{}",
116            Self::escape_field(&entry.id),
117            Self::escape_field(&format!("{:?}", entry.source)),
118            Self::escape_field(&entry.timestamp.format("%Y-%m-%d %H:%M:%S").to_string()),
119            Self::escape_field(&entry.prompt),
120            Self::escape_field(&entry.response)
121        )?;
122        Ok(())
123    }
124
125    fn config(&self) -> &FormatterConfig {
126        &self.config
127    }
128
129    fn set_config(&mut self, config: FormatterConfig) {
130        self.config = config;
131    }
132}
133
134/// Table formatter for terminal output
135#[derive(Debug, Clone, Default)]
136pub struct TableFormatter {
137    config: FormatterConfig,
138}
139
140impl TableFormatter {
141    /// Create a new formatter instance
142    pub fn new() -> Self {
143        Self::default()
144    }
145}
146
147impl Formatter for TableFormatter {
148    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
149        // Simple table format for now
150        for entry in entries {
151            writeln!(
152                writer,
153                "[{}] {:?}",
154                entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
155                entry.source
156            )?;
157            writeln!(
158                writer,
159                "  Prompt: {}",
160                self.config.truncate_text(&entry.prompt, 80)
161            )?;
162            writeln!(writer)?;
163        }
164        Ok(())
165    }
166
167    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
168        writeln!(
169            writer,
170            "[{}] {:?}",
171            entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
172            entry.source
173        )?;
174        writeln!(
175            writer,
176            "  Prompt: {}",
177            self.config.truncate_text(&entry.prompt, 80)
178        )?;
179        writeln!(writer)?;
180        Ok(())
181    }
182
183    fn config(&self) -> &FormatterConfig {
184        &self.config
185    }
186
187    fn set_config(&mut self, config: FormatterConfig) {
188        self.config = config;
189    }
190}
191
192/// AI-optimized compact formatter
193#[derive(Debug, Clone, Default)]
194pub struct AiFormatter {
195    config: FormatterConfig,
196}
197
198impl AiFormatter {
199    /// Create a new formatter instance
200    pub fn new() -> Self {
201        Self::default()
202    }
203}
204
205impl Formatter for AiFormatter {
206    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
207        for entry in entries {
208            writeln!(
209                writer,
210                "[{}] {:?}: {}",
211                entry.timestamp.format("%Y-%m-%d %H:%M"),
212                entry.source,
213                self.config.truncate_text(&entry.prompt, 60)
214            )?;
215            writeln!(
216                writer,
217                "  -> {}",
218                self.config.truncate_text(&entry.response, 100)
219            )?;
220            writeln!(writer)?;
221        }
222        Ok(())
223    }
224
225    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
226        writeln!(
227            writer,
228            "[{}] {:?}: {}",
229            entry.timestamp.format("%Y-%m-%d %H:%M"),
230            entry.source,
231            self.config.truncate_text(&entry.prompt, 60)
232        )?;
233        writeln!(
234            writer,
235            "  -> {}",
236            self.config.truncate_text(&entry.response, 100)
237        )?;
238        writeln!(writer)?;
239        Ok(())
240    }
241
242    fn config(&self) -> &FormatterConfig {
243        &self.config
244    }
245
246    fn set_config(&mut self, config: FormatterConfig) {
247        self.config = config;
248    }
249}
250
251/// Statistics summary formatter
252#[derive(Debug, Clone, Default)]
253pub struct StatsFormatter {
254    config: FormatterConfig,
255}
256
257impl StatsFormatter {
258    /// Create a new formatter instance
259    pub fn new() -> Self {
260        Self::default()
261    }
262}
263
264impl Formatter for StatsFormatter {
265    fn format<W: Write>(&self, entries: &[Entry], writer: &mut W) -> Result<()> {
266        writeln!(writer, "=== Summary Statistics ===")?;
267        writeln!(writer, "Total entries: {}", entries.len())?;
268
269        let mut by_source = std::collections::HashMap::new();
270        for entry in entries {
271            *by_source.entry(format!("{:?}", entry.source)).or_insert(0) += 1;
272        }
273
274        writeln!(writer, "\nBy source:")?;
275        for (source, count) in by_source {
276            writeln!(writer, "  {}: {}", source, count)?;
277        }
278
279        Ok(())
280    }
281
282    fn format_one<W: Write>(&self, entry: &Entry, writer: &mut W) -> Result<()> {
283        writeln!(
284            writer,
285            "[{}] {:?}",
286            entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
287            entry.source
288        )?;
289        Ok(())
290    }
291
292    fn config(&self) -> &FormatterConfig {
293        &self.config
294    }
295
296    fn set_config(&mut self, config: FormatterConfig) {
297        self.config = config;
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use cai_core::{Entry, Metadata, Source};
305    use chrono::Utc;
306
307    fn mock_entry() -> Entry {
308        Entry {
309            id: "test-1".to_string(),
310            source: Source::Claude,
311            timestamp: Utc::now(),
312            prompt: "Write a function".to_string(),
313            response: "Here is the function".to_string(),
314            metadata: Metadata {
315                file_path: Some("src/main.rs".to_string()),
316                repo_url: None,
317                commit_hash: None,
318                language: Some("Rust".to_string()),
319                extra: std::collections::HashMap::new(),
320            },
321        }
322    }
323
324    #[test]
325    fn test_json_formatter() {
326        let formatter = JsonFormatter::default();
327        let entries = vec![mock_entry()];
328        let mut buf = Vec::new();
329        formatter.format(&entries, &mut buf).unwrap();
330        let output = String::from_utf8(buf).unwrap();
331        // Verify valid JSON output
332        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
333        assert_eq!(parsed.as_array().unwrap().len(), 1);
334    }
335
336    #[test]
337    fn test_jsonl_formatter() {
338        let formatter = JsonlFormatter::default();
339        let entries = vec![mock_entry()];
340        let mut buf = Vec::new();
341        formatter.format(&entries, &mut buf).unwrap();
342        let output = String::from_utf8(buf).unwrap();
343        // Verify valid JSONL output (one JSON per line)
344        for line in output.lines() {
345            let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
346            assert!(parsed.is_object());
347        }
348        assert_eq!(output.lines().count(), 1);
349    }
350
351    #[test]
352    fn test_csv_formatter() {
353        let formatter = CsvFormatter::default();
354        let entries = vec![mock_entry()];
355        let mut buf = Vec::new();
356        formatter.format(&entries, &mut buf).unwrap();
357        let output = String::from_utf8(buf).unwrap();
358        assert!(output.starts_with("id,source,timestamp"));
359        assert!(output.contains("test-1"));
360    }
361
362    #[test]
363    fn test_csv_escape() {
364        assert_eq!(CsvFormatter::escape_field("simple"), "simple");
365        assert_eq!(CsvFormatter::escape_field("with, comma"), "\"with, comma\"");
366        assert_eq!(
367            CsvFormatter::escape_field("with\"quote"),
368            "\"with\"\"quote\""
369        );
370    }
371
372    #[test]
373    fn test_ai_formatter() {
374        let formatter = AiFormatter::default();
375        let entry = mock_entry();
376        let mut buf = Vec::new();
377        formatter.format_one(&entry, &mut buf).unwrap();
378        let output = String::from_utf8(buf).unwrap();
379        assert!(output.contains("Write a function"));
380        assert!(output.contains("->"));
381    }
382
383    #[test]
384    fn test_stats_formatter() {
385        let formatter = StatsFormatter::default();
386        let entries = vec![mock_entry()];
387        let mut buf = Vec::new();
388        formatter.format(&entries, &mut buf).unwrap();
389        let output = String::from_utf8(buf).unwrap();
390        assert!(output.contains("Summary Statistics"));
391        assert!(output.contains("By source"));
392        assert!(output.contains("Claude"));
393    }
394
395    #[test]
396    fn test_truncate() {
397        let config = FormatterConfig::default();
398        assert_eq!(config.truncate_text("short", 100), "short");
399        assert_eq!(config.truncate_text("hello world", 8), "hello...");
400        assert_eq!(config.truncate_text("test", 0), "test");
401    }
402}