1use serde_json::Value;
6
7pub const EXPORT_CAP: u64 = 10_000;
9
10pub fn rows_to_csv(headers: &[String], rows: &[Value]) -> String {
13 let mut out = String::new();
14 out.push_str(
15 &headers
16 .iter()
17 .map(|h| csv_escape(h))
18 .collect::<Vec<_>>()
19 .join(","),
20 );
21 out.push('\n');
22
23 for row in rows {
24 let line = headers
25 .iter()
26 .map(|h| csv_escape(&cell_value(row.get(h))))
27 .collect::<Vec<_>>()
28 .join(",");
29 out.push_str(&line);
30 out.push('\n');
31 }
32 out
33}
34
35fn cell_value(v: Option<&Value>) -> String {
36 match v {
37 Some(Value::String(s)) => s.clone(),
38 Some(Value::Null) | None => String::new(),
39 Some(other) => other.to_string(),
40 }
41}
42
43fn csv_escape(s: &str) -> String {
44 if s.contains(['"', ',', '\n', '\r']) {
45 format!("\"{}\"", s.replace('"', "\"\""))
46 } else {
47 s.to_string()
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use serde_json::json;
55
56 #[test]
57 fn csv_has_header_and_escapes() {
58 let headers = vec!["id".to_string(), "name".to_string()];
59 let rows = vec![
60 json!({"id": 1, "name": "Ada"}),
61 json!({"id": 2, "name": "a, b \"c\""}),
62 ];
63 let csv = rows_to_csv(&headers, &rows);
64 let lines: Vec<&str> = csv.lines().collect();
65 assert_eq!(lines[0], "id,name");
66 assert_eq!(lines[1], "1,Ada");
67 assert_eq!(lines[2], "2,\"a, b \"\"c\"\"\"");
68 }
69}