systemprompt_cli/commands/analytics/shared/
export.rs1use 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}