Skip to main content

adminx_core/
export.rs

1// adminx-core/src/export.rs
2//
3// Serialize a set of rows for download as JSON or CSV.
4
5use serde_json::Value;
6
7/// Maximum rows included in an export (guards memory / response size).
8pub const EXPORT_CAP: u64 = 10_000;
9
10/// Render rows to CSV with the given column order. Values are stringified;
11/// strings/numbers/bools render plainly, other JSON as compact text.
12pub 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}