Skip to main content

systemprompt_cli/commands/analytics/shared/
export.rs

1use anyhow::{Context, Result};
2use serde::Serialize;
3use std::fs::{self, File};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7use systemprompt_models::AppPaths;
8
9pub fn resolve_export_path(user_path: &Path) -> Result<PathBuf> {
10    if user_path.is_absolute()
11        || user_path
12            .parent()
13            .is_some_and(|p| !p.as_os_str().is_empty())
14    {
15        return Ok(user_path.to_path_buf());
16    }
17
18    let exports_dir = AppPaths::get()
19        .context("AppPaths not initialized - use an absolute path for export")?
20        .storage()
21        .exports()
22        .to_path_buf();
23
24    Ok(exports_dir.join(user_path))
25}
26
27pub fn ensure_export_dir(path: &Path) -> Result<()> {
28    if let Some(parent) = path.parent() {
29        if !parent.exists() {
30            fs::create_dir_all(parent).context("Failed to create export directory")?;
31        }
32    }
33    Ok(())
34}
35
36pub fn export_to_csv<T: Serialize>(data: &[T], path: &Path) -> Result<()> {
37    ensure_export_dir(path)?;
38    let file = File::create(path).context("Failed to create export file")?;
39    let mut writer = std::io::BufWriter::new(file);
40
41    if data.is_empty() {
42        return Ok(());
43    }
44
45    let json_value = serde_json::to_value(&data[0])?;
46    if let serde_json::Value::Object(obj) = json_value {
47        let headers: Vec<&str> = obj.keys().map(String::as_str).collect();
48        writeln!(writer, "{}", headers.join(","))?;
49    }
50
51    for record in data {
52        let json = serde_json::to_value(record)?;
53        if let serde_json::Value::Object(obj) = json {
54            let values: Vec<String> = obj
55                .values()
56                .map(|v| match v {
57                    serde_json::Value::String(s) => escape_csv_field(s),
58                    serde_json::Value::Null => String::new(),
59                    _ => v.to_string(),
60                })
61                .collect();
62            writeln!(writer, "{}", values.join(","))?;
63        }
64    }
65
66    writer.flush()?;
67    Ok(())
68}
69
70pub fn export_single_to_csv<T: Serialize>(data: &T, path: &Path) -> Result<()> {
71    ensure_export_dir(path)?;
72    let file = File::create(path).context("Failed to create export file")?;
73    let mut writer = std::io::BufWriter::new(file);
74
75    let json = serde_json::to_value(data)?;
76    if let serde_json::Value::Object(obj) = json {
77        let headers: Vec<&str> = obj.keys().map(String::as_str).collect();
78        writeln!(writer, "{}", headers.join(","))?;
79
80        let values: Vec<String> = obj
81            .values()
82            .map(|v| match v {
83                serde_json::Value::String(s) => escape_csv_field(s),
84                serde_json::Value::Null => String::new(),
85                _ => v.to_string(),
86            })
87            .collect();
88        writeln!(writer, "{}", values.join(","))?;
89    }
90
91    writer.flush()?;
92    Ok(())
93}
94
95fn escape_csv_field(s: &str) -> String {
96    if s.contains(',') || s.contains('"') || s.contains('\n') {
97        format!("\"{}\"", s.replace('"', "\"\""))
98    } else {
99        s.to_string()
100    }
101}
102
103#[derive(Debug)]
104pub struct CsvBuilder {
105    headers: Vec<String>,
106    rows: Vec<Vec<String>>,
107}
108
109impl CsvBuilder {
110    pub const fn new() -> Self {
111        Self {
112            headers: Vec::new(),
113            rows: Vec::new(),
114        }
115    }
116
117    pub fn headers(mut self, headers: Vec<&str>) -> Self {
118        self.headers = headers.into_iter().map(String::from).collect();
119        self
120    }
121
122    pub fn add_row(&mut self, row: Vec<String>) {
123        self.rows.push(row);
124    }
125
126    pub fn write_to_file(&self, path: &Path) -> Result<()> {
127        ensure_export_dir(path)?;
128        let mut file = File::create(path).context("Failed to create export file")?;
129
130        writeln!(file, "{}", self.headers.join(","))?;
131
132        for row in &self.rows {
133            let escaped: Vec<String> = row.iter().map(|cell| escape_csv_field(cell)).collect();
134            writeln!(file, "{}", escaped.join(","))?;
135        }
136
137        file.flush().context("Failed to flush export file")?;
138        Ok(())
139    }
140}
141
142impl Default for CsvBuilder {
143    fn default() -> Self {
144        Self::new()
145    }
146}