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