1use crate::formatter::Truncate;
4use crate::{Formatter, FormatterConfig};
5use cai_core::Entry;
6use cai_core::Result;
7use std::io::Write;
8
9#[derive(Debug, Clone, Default)]
11pub struct JsonFormatter {
12 config: FormatterConfig,
13}
14
15impl JsonFormatter {
16 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#[derive(Debug, Clone, Default)]
45pub struct JsonlFormatter {
46 config: FormatterConfig,
47}
48
49impl JsonlFormatter {
50 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#[derive(Debug, Clone, Default)]
81pub struct CsvFormatter {
82 config: FormatterConfig,
83}
84
85impl CsvFormatter {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 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 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#[derive(Debug, Clone, Default)]
136pub struct TableFormatter {
137 config: FormatterConfig,
138}
139
140impl TableFormatter {
141 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 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#[derive(Debug, Clone, Default)]
194pub struct AiFormatter {
195 config: FormatterConfig,
196}
197
198impl AiFormatter {
199 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#[derive(Debug, Clone, Default)]
253pub struct StatsFormatter {
254 config: FormatterConfig,
255}
256
257impl StatsFormatter {
258 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 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 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}