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