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