browser_automation_cli/
sheet_local.rs1use std::fs;
6use std::path::Path;
7
8use rust_xlsxwriter::Workbook;
9use serde_json::Value;
10
11use crate::error::{CliError, ErrorKind};
12
13pub fn sheet_write(input: &Path, out: &Path, sheet_name: &str) -> Result<Value, CliError> {
15 if sheet_name.trim().is_empty() {
16 return Err(CliError::new(
17 ErrorKind::Usage,
18 "sheet name must not be empty",
19 ));
20 }
21 let ext = input
22 .extension()
23 .and_then(|e| e.to_str())
24 .unwrap_or("")
25 .to_ascii_lowercase();
26 let rows = match ext.as_str() {
27 "csv" | "tsv" => read_csv_rows(input, if ext == "tsv" { b'\t' } else { b',' })?,
28 "json" => read_json_rows(input)?,
29 other => {
30 return Err(CliError::with_suggestion(
31 ErrorKind::Usage,
32 format!("unsupported sheet-write input extension: {other}"),
33 "Pass a .csv, .tsv, or .json (array of objects) file",
34 ));
35 }
36 };
37 if rows.is_empty() {
38 return Err(CliError::new(ErrorKind::Data, "no rows to write"));
39 }
40
41 let mut workbook = Workbook::new();
42 let worksheet = workbook
43 .add_worksheet()
44 .set_name(sheet_name)
45 .map_err(|e| CliError::new(ErrorKind::Software, format!("worksheet name: {e}")))?;
46
47 for (r, row) in rows.iter().enumerate() {
48 for (c, cell) in row.iter().enumerate() {
49 worksheet
50 .write_string(r as u32, c as u16, cell)
51 .map_err(|e| CliError::new(ErrorKind::Software, format!("write cell: {e}")))?;
52 }
53 }
54
55 let parent = out.parent().unwrap_or_else(|| Path::new("."));
57 let tmp = parent.join(format!(
58 ".browser-automation-cli-xlsx-{}.tmp",
59 std::process::id()
60 ));
61 workbook
62 .save(&tmp)
63 .map_err(|e| CliError::new(ErrorKind::Io, format!("xlsx save: {e}")))?;
64 fs::rename(&tmp, out).map_err(|e| {
65 let _ = fs::remove_file(&tmp);
66 CliError::new(
67 ErrorKind::Io,
68 format!("rename xlsx {} → {}: {e}", tmp.display(), out.display()),
69 )
70 })?;
71
72 Ok(serde_json::json!({
73 "ok": true,
74 "path": out.display().to_string(),
75 "rows": rows.len(),
76 "cols": rows.first().map(|r| r.len()).unwrap_or(0),
77 "sheet": sheet_name,
78 "chrome": false,
79 }))
80}
81
82fn read_csv_rows(path: &Path, delim: u8) -> Result<Vec<Vec<String>>, CliError> {
83 let raw = fs::read(path)
84 .map_err(|e| CliError::new(ErrorKind::Io, format!("read {}: {e}", path.display())))?;
85 let mut rdr = csv::ReaderBuilder::new()
86 .delimiter(delim)
87 .flexible(true)
88 .has_headers(false)
89 .from_reader(raw.as_slice());
90 let mut rows = Vec::new();
91 for rec in rdr.records() {
92 let rec = rec.map_err(|e| CliError::new(ErrorKind::Data, format!("csv: {e}")))?;
93 rows.push(rec.iter().map(|s| s.to_string()).collect());
94 }
95 Ok(rows)
96}
97
98fn read_json_rows(path: &Path) -> Result<Vec<Vec<String>>, CliError> {
99 let raw = fs::read_to_string(path)
100 .map_err(|e| CliError::new(ErrorKind::Io, format!("read {}: {e}", path.display())))?;
101 let v: Value = serde_json::from_str(&raw)
102 .map_err(|e| CliError::new(ErrorKind::Data, format!("json: {e}")))?;
103 let arr = v.as_array().ok_or_else(|| {
104 CliError::with_suggestion(
105 ErrorKind::Data,
106 "JSON input must be an array of objects or array of arrays",
107 "Example: [{\"a\":1,\"b\":2},{\"a\":3,\"b\":4}]",
108 )
109 })?;
110 if arr.is_empty() {
111 return Ok(Vec::new());
112 }
113 if arr[0].is_array() {
115 let mut rows = Vec::new();
116 for row in arr {
117 let cells = row
118 .as_array()
119 .ok_or_else(|| CliError::new(ErrorKind::Data, "expected array row"))?
120 .iter()
121 .map(json_cell)
122 .collect();
123 rows.push(cells);
124 }
125 return Ok(rows);
126 }
127 let first = arr[0]
129 .as_object()
130 .ok_or_else(|| CliError::new(ErrorKind::Data, "expected object row"))?;
131 let mut keys: Vec<String> = first.keys().cloned().collect();
132 keys.sort();
133 let mut rows = vec![keys.clone()];
134 for obj in arr {
135 let map = obj
136 .as_object()
137 .ok_or_else(|| CliError::new(ErrorKind::Data, "expected object row"))?;
138 let row = keys
139 .iter()
140 .map(|k| map.get(k).map(json_cell).unwrap_or_default())
141 .collect();
142 rows.push(row);
143 }
144 Ok(rows)
145}
146
147fn json_cell(v: &Value) -> String {
148 match v {
149 Value::Null => String::new(),
150 Value::String(s) => s.clone(),
151 Value::Bool(b) => b.to_string(),
152 Value::Number(n) => n.to_string(),
153 other => other.to_string(),
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn writes_csv_to_xlsx() {
163 let dir = tempfile::tempdir().unwrap();
164 let csv_path = dir.path().join("t.csv");
165 fs::write(&csv_path, "a,b\n1,2\n").unwrap();
166 let out = dir.path().join("t.xlsx");
167 let v = sheet_write(&csv_path, &out, "Sheet1").unwrap();
168 assert!(out.exists());
169 assert_eq!(v.get("rows").and_then(|r| r.as_u64()), Some(2));
170 let bytes = fs::read(&out).unwrap();
172 assert!(bytes.starts_with(b"PK"));
173 }
174}