Skip to main content

systemprompt_cli/commands/analytics/shared/
export.rs

1use anyhow::{Context, Result};
2use serde::Serialize;
3use std::fs::File;
4use std::io::Write;
5use std::path::Path;
6
7pub fn export_to_csv<T: Serialize>(data: &[T], path: &Path) -> Result<()> {
8    let file = File::create(path).context("Failed to create export file")?;
9    let mut writer = std::io::BufWriter::new(file);
10
11    if data.is_empty() {
12        return Ok(());
13    }
14
15    let json_value = serde_json::to_value(&data[0])?;
16    if let serde_json::Value::Object(obj) = json_value {
17        let headers: Vec<&str> = obj.keys().map(String::as_str).collect();
18        writeln!(writer, "{}", headers.join(","))?;
19    }
20
21    for record in data {
22        let json = serde_json::to_value(record)?;
23        if let serde_json::Value::Object(obj) = json {
24            let values: Vec<String> = obj
25                .values()
26                .map(|v| match v {
27                    serde_json::Value::String(s) => escape_csv_field(s),
28                    serde_json::Value::Null => String::new(),
29                    _ => v.to_string(),
30                })
31                .collect();
32            writeln!(writer, "{}", values.join(","))?;
33        }
34    }
35
36    writer.flush()?;
37    Ok(())
38}
39
40pub fn export_single_to_csv<T: Serialize>(data: &T, path: &Path) -> Result<()> {
41    let file = File::create(path).context("Failed to create export file")?;
42    let mut writer = std::io::BufWriter::new(file);
43
44    let json = serde_json::to_value(data)?;
45    if let serde_json::Value::Object(obj) = json {
46        let headers: Vec<&str> = obj.keys().map(String::as_str).collect();
47        writeln!(writer, "{}", headers.join(","))?;
48
49        let values: Vec<String> = obj
50            .values()
51            .map(|v| match v {
52                serde_json::Value::String(s) => escape_csv_field(s),
53                serde_json::Value::Null => String::new(),
54                _ => v.to_string(),
55            })
56            .collect();
57        writeln!(writer, "{}", values.join(","))?;
58    }
59
60    writer.flush()?;
61    Ok(())
62}
63
64fn escape_csv_field(s: &str) -> String {
65    if s.contains(',') || s.contains('"') || s.contains('\n') {
66        format!("\"{}\"", s.replace('"', "\"\""))
67    } else {
68        s.to_string()
69    }
70}
71
72#[derive(Debug)]
73pub struct CsvBuilder {
74    headers: Vec<String>,
75    rows: Vec<Vec<String>>,
76}
77
78impl CsvBuilder {
79    pub const fn new() -> Self {
80        Self {
81            headers: Vec::new(),
82            rows: Vec::new(),
83        }
84    }
85
86    pub fn headers(mut self, headers: Vec<&str>) -> Self {
87        self.headers = headers.into_iter().map(String::from).collect();
88        self
89    }
90
91    pub fn add_row(&mut self, row: Vec<String>) {
92        self.rows.push(row);
93    }
94
95    pub fn write_to_file(&self, path: &Path) -> Result<()> {
96        let mut file = File::create(path).context("Failed to create export file")?;
97
98        writeln!(file, "{}", self.headers.join(","))?;
99
100        for row in &self.rows {
101            let escaped: Vec<String> = row.iter().map(|cell| escape_csv_field(cell)).collect();
102            writeln!(file, "{}", escaped.join(","))?;
103        }
104
105        Ok(())
106    }
107}
108
109impl Default for CsvBuilder {
110    fn default() -> Self {
111        Self::new()
112    }
113}